diff --git a/src/bootsupport/modules/argparsingtest-0.1.0.tm b/src/bootsupport/modules/argparsingtest-0.1.0.tm index 40366143..b97d1b4e 100644 --- a/src/bootsupport/modules/argparsingtest-0.1.0.tm +++ b/src/bootsupport/modules/argparsingtest-0.1.0.tm @@ -321,6 +321,7 @@ namespace eval argparsingtest { punk::args::define { @id -id ::argparsingtest::test1_punkargs2 @cmd -name argtest4 -help "test of punk::args::parse comparative performance" + @leaders -min 0 -max 0 @opts -anyopts 0 -return -default string -type string -frametype -default \uFFEF -type string @@ -333,10 +334,10 @@ namespace eval argparsingtest { -1 -default 1 -type boolean -2 -default 2 -type integer -3 -default 3 -type integer - @values + @values -min 0 -max 0 } proc test1_punkargs2 {args} { - set argd [punk::args::get_by_id ::argparsingtest::test1_punkargs2 $args] + set argd [punk::args::parse $args withid ::argparsingtest::test1_punkargs2] return [tcl::dict::get $argd opts] } @@ -494,6 +495,38 @@ namespace eval argparsingtest { }]] return $argd } + proc test_multiline2 {args} { + set t3 [textblock::frame t3] + set argd [punk::args::parse $args withdef { + -template1 -default { + ****** + * t1 * + ****** + } + -template2 -default { ------ + ****** + * t2 * + ******} + -template3 -default {$t3} + #substituted or literal values with newlines - no autoindent applied - caller will have to pad appropriately + -template3b -default { + ${$t3} + ----------------- + ${$t3} + abc\ndef + } + -template4 -default "****** + * t4 * + ******" + -template5 -default " + a + ${$t3} + c + " + -flag -default 0 -type boolean + }] + return $argd + } #proc sample1 {p1 n args} { # #*** !doctools diff --git a/src/bootsupport/modules/dictn-0.1.1.tm b/src/bootsupport/modules/dictn-0.1.1.tm deleted file mode 100644 index c9ef87f2..00000000 --- a/src/bootsupport/modules/dictn-0.1.1.tm +++ /dev/null @@ -1,349 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2023 -# -# @@ Meta Begin -# Application dictn 0.1.1 -# Meta platform tcl -# Meta license -# @@ Meta End - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -##e.g package require frobz - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval dictn { - namespace export {[a-z]*} - namespace ensemble create -} - - -## ::dictn::append -#This can of course 'ruin' a nested dict if applied to the wrong element -# - i.e using the string op 'append' on an element that is itself a nested dict is analogous to the standard Tcl: -# %set list {a b {c d}} -# %append list x -# a b {c d}x -# IOW - don't do that unless you really know that's what you want. -# -proc ::dictn::append {dictvar path {value {}}} { - if {[llength $path] == 1} { - uplevel 1 [list dict append $dictvar $path $value] - } else { - upvar 1 $dictvar dvar - - ::set str [dict get $dvar {*}$path] - append str $val - dict set dvar {*}$path $str - } -} - -proc ::dictn::create {args} { - ::set data {} - foreach {path val} $args { - dict set data {*}$path $val - } - return $data -} - -proc ::dictn::exists {dictval path} { - return [dict exists $dictval {*}$path] -} - -proc ::dictn::filter {dictval path filterType args} { - ::set sub [dict get $dictval {*}$path] - dict filter $sub $filterType {*}$args -} - -proc ::dictn::for {keyvalvars dictval path body} { - ::set sub [dict get $dictval {*}$path] - dict for $keyvalvars $sub $body -} - -proc ::dictn::get {dictval {path {}}} { - return [dict get $dictval {*}$path] -} - -proc ::dictn::getdef {dictval path default} { - return [dict getdef $dictval {*}$path $default] -} - -proc ::dictn::getwithdefault {dictval path default} { - return [dict getdef $dictval {*}$path $default] -} - -if {[info commands ::tcl::dict::getdef] ne ""} { - proc ::dictn::incr {dictvar path {increment {}} } { - if {$increment eq ""} { - ::set increment 1 - } - if {[llength $path] == 1} { - uplevel 1 [list dict incr $dictvar $path $increment] - } else { - upvar 1 $dictvar dvar - if {![::info exists dvar]} { - dict set dvar {*}$path $increment - } else { - ::set newval [expr {[dict getdef $dvar {*}$path 0] + $increment}] - dict set dvar {*}$path $newval - } - return $dvar - } - } -} else { - proc ::dictn::incr {dictvar path {increment {}} } { - if {$increment eq ""} { - ::set increment 1 - } - if {[llength $path] == 1} { - uplevel 1 [list dict incr $dictvar $path $increment] - } else { - upvar 1 $dictvar dvar - if {![::info exists dvar]} { - dict set dvar {*}$path $increment - } else { - if {![dict exists $dvar {*}$path]} { - ::set val 0 - } else { - ::set val [dict get $dvar {*}$path] - } - ::set newval [expr {$val + $increment}] - dict set dvar {*}$path $newval - } - return $dvar - } - } -} - -proc ::dictn::info {dictval {path {}}} { - if {![string length $path]} { - return [dict info $dictval] - } else { - ::set sub [dict get $dictval {*}$path] - return [dict info $sub] - } -} - -proc ::dictn::keys {dictval {path {}} {glob {}}} { - ::set sub [dict get $dictval {*}$path] - if {[string length $glob]} { - return [dict keys $sub $glob] - } else { - return [dict keys $sub] - } -} - -proc ::dictn::lappend {dictvar path args} { - if {[llength $path] == 1} { - uplevel 1 [list dict lappend $dictvar $path {*}$args] - } else { - upvar 1 $dictvar dvar - - ::set list [dict get $dvar {*}$path] - ::lappend list {*}$args - dict set dvar {*}$path $list - } -} - -proc ::dictn::merge {args} { - error "nested merge not yet supported" -} - -#dictn remove dictionaryValue ?path ...? -proc ::dictn::remove {dictval args} { - ::set basic [list] ;#buffer basic (1element path) removals to do in a single call. - - foreach path $args { - if {[llength $path] == 1} { - ::lappend basic $path - } else { - #extract,modify,replace - ::set subpath [lrange $path 0 end-1] - - ::set sub [dict get $dictval {*}$subpath] - ::set sub [dict remove $sub [lindex $path end]] - - dict set dictval {*}$subpath $sub - } - } - - if {[llength $basic]} { - return [dict remove $dictval {*}$basic] - } else { - return $dictval - } -} - - -proc ::dictn::replace {dictval args} { - ::set basic [list] ;#buffer basic (1element path) replacements to do in a single call. - - foreach {path val} $args { - if {[llength $path] == 1} { - ::lappend basic $path $val - } else { - #extract,modify,replace - ::set subpath [lrange $path 0 end-1] - - ::set sub [dict get $dictval {*}$subpath] - ::set sub [dict replace $sub [lindex $path end] $val] - - dict set dictval {*}$subpath $sub - } - } - - - if {[llength $basic]} { - return [dict replace $dictval {*}$basic] - } else { - return $dictval - } -} - - -proc ::dictn::set {dictvar path newval} { - upvar 1 $dictvar dvar - return [dict set dvar {*}$path $newval] -} - -proc ::dictn::size {dictval {path {}}} { - return [dict size [dict get $dictval {*}$path]] -} - -proc ::dictn::unset {dictvar path} { - upvar 1 $dictvar dvar - return [dict unset dvar {*}$path -} - -proc ::dictn::update {dictvar args} { - ::set body [lindex $args end] - ::set maplist [lrange $args 0 end-1] - - upvar 1 $dictvar dvar - foreach {path var} $maplist { - if {[dict exists $dvar {*}$path]} { - uplevel 1 [list set $var [dict get $dvar $path]] - } - } - - catch {uplevel 1 $body} result - - foreach {path var} $maplist { - if {[dict exists $dvar {*}$path]} { - upvar 1 $var $var - if {![::info exists $var]} { - uplevel 1 [list dict unset $dictvar {*}$path] - } else { - uplevel 1 [list dict set $dictvar {*}$path [::set $var]] - } - } - } - return $result -} - -#an experiment. -proc ::dictn::Applyupdate {dictvar args} { - ::set body [lindex $args end] - ::set maplist [lrange $args 0 end-1] - - upvar 1 $dictvar dvar - - ::set headscript "" - ::set i 0 - foreach {path var} $maplist { - if {[dict exists $dvar {*}$path]} { - #uplevel 1 [list set $var [dict get $dvar $path]] - ::lappend arglist $var - ::lappend vallist [dict get $dvar {*}$path] - ::append headscript [string map [list %i% $i %v% $var] {upvar 1 %v% %v%; set %v% [lindex $args %i%]} ] - ::append headscript \n - ::incr i - } - } - - ::set body $headscript\r\n$body - - puts stderr "BODY: $body" - - #set result [apply [list args $body] {*}$vallist] - catch {apply [list args $body] {*}$vallist} result - - foreach {path var} $maplist { - if {[dict exists $dvar {*}$path] && [::info exists $var]} { - dict set dvar {*}$path [::set $var] - } - } - return $result -} - -proc ::dictn::values {dictval {path {}} {glob {}}} { - ::set sub [dict get $dictval {*}$path] - if {[string length $glob]} { - return [dict values $sub $glob] - } else { - return [dict values $sub] - } -} - -# Standard form: -#'dictn with dictVariable path body' -# -# Extended form: -#'dictn with dictVariable path arrayVariable body' -# -proc ::dictn::with {dictvar path args} { - if {[llength $args] == 1} { - ::set body [lindex $args 0] - return [uplevel 1 [list dict with $dictvar {*}$path $body]] - } else { - upvar 1 $dictvar dvar - ::lassign $args arrayname body - - upvar 1 $arrayname arr - array set arr [dict get $dvar {*}$path] - ::set prevkeys [array names arr] - - catch {uplevel 1 $body} result - - - foreach k $prevkeys { - if {![::info exists arr($k)]} { - dict unset $dvar {*}$path $k - } - } - foreach k [array names arr] { - dict set $dvar {*}$path $k $arr($k) - } - - return $result - } -} - - - - - - - - - - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide dictn [namespace eval dictn { - variable version - ::set version 0.1.1 -}] -return \ No newline at end of file diff --git a/src/bootsupport/modules/include_modules.config b/src/bootsupport/modules/include_modules.config index afd1e8f2..226e17de 100644 --- a/src/bootsupport/modules/include_modules.config +++ b/src/bootsupport/modules/include_modules.config @@ -46,6 +46,7 @@ set bootsupport_modules [list\ modules punkcheck\ modules punkcheck::cli\ modules punk::aliascore\ + modules punk::ansi::colourmap\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -61,6 +62,7 @@ set bootsupport_modules [list\ modules punk::fileline\ modules punk::docgen\ modules punk::lib\ + modules punk::libunknown\ modules punk::mix\ modules punk::mix::base\ modules punk::mix::cli\ diff --git a/src/bootsupport/modules/modpod-0.1.2.tm b/src/bootsupport/modules/modpod-0.1.2.tm deleted file mode 100644 index aa27ebce..00000000 --- a/src/bootsupport/modules/modpod-0.1.2.tm +++ /dev/null @@ -1,702 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application modpod 0.1.2 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin modpod_module_modpod 0 0.1.2] -#[copyright "2024"] -#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] -#[moddesc {-}] [comment {-- Description at end of page heading --}] -#[require modpod] -#[keywords module] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of modpod -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by modpod -#[list_begin itemized] - -package require Tcl 8.6- -package require struct::set ;#review -package require punk::lib -package require punk::args -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval modpod::class { - #*** !doctools - #[subsection {Namespace modpod::class}] - #[para] class definitions - if {[info commands [namespace current]::interface_sample1] eq ""} { - #*** !doctools - #[list_begin enumerated] - - # oo::class create interface_sample1 { - # #*** !doctools - # #[enum] CLASS [class interface_sample1] - # #[list_begin definitions] - - # method test {arg1} { - # #*** !doctools - # #[call class::interface_sample1 [method test] [arg arg1]] - # #[para] test method - # puts "test: $arg1" - # } - - # #*** !doctools - # #[list_end] [comment {-- end definitions interface_sample1}] - # } - - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - } -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval modpod { - namespace export {[a-z]*}; # Convention: export all lowercase - - variable connected - if {![info exists connected(to)]} { - set connected(to) list - } - variable modpodscript - set modpodscript [info script] - if {[string tolower [file extension $modpodscript]] eq ".tcl"} { - set connected(self) [file dirname $modpodscript] - } else { - #expecting a .tm - set connected(self) $modpodscript - } - variable loadables [info sharedlibextension] - variable sourceables {.tcl .tk} ;# .tm ? - - #*** !doctools - #[subsection {Namespace modpod}] - #[para] Core API functions for modpod - #[list_begin definitions] - - - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - #old tar connect mechanism - review - not needed? - proc connect {args} { - puts stderr "modpod::connect--->>$args" - set argd [punk::args::get_dict { - @id -id ::modpod::connect - -type -default "" - @values -min 1 -max 1 - path -type string -minsize 1 -help "path to .tm file or toplevel .tcl script within #modpod-- folder (unwrapped modpod)" - } $args] - catch { - punk::lib::showdict $argd ;#heavy dependencies - } - set opt_path [dict get $argd values path] - variable connected - set original_connectpath $opt_path - set modpodpath [modpod::system::normalize $opt_path] ;# - - if {$modpodpath in $connected(to)} { - return [dict create ok ALREADY_CONNECTED] - } - lappend connected(to) $modpodpath - - set connected(connectpath,$opt_path) $original_connectpath - set is_sourced [expr {[file normalize $modpodpath] eq [file normalize [info script]]}] - - set connected(location,$modpodpath) [file dirname $modpodpath] - set connected(startdata,$modpodpath) -1 - set connected(type,$modpodpath) [dict get $argd opts -type] - set connected(fh,$modpodpath) "" - - if {[string range [file tail $modpodpath] 0 7] eq "#modpod-"} { - set connected(type,$modpodpath) "unwrapped" - lassign [::split [file tail [file dirname $modpodpath]] -] connected(package,$modpodpath) connected(version,$modpodpath) - set this_pkg_tm_folder [file dirname [file dirname $modpodpath]] - - } else { - #connect to .tm but may still be unwrapped version available - lassign [::split [file rootname [file tail $modpodath]] -] connected(package,$modpodpath) connected(version,$modpodpath) - set this_pkg_tm_folder [file dirname $modpodpath] - if {$connected(type,$modpodpath) ne "unwrapped"} { - #Not directly connected to unwrapped version - but may still be redirected there - set unwrappedFolder [file join $connected(location,$modpodpath) #modpod-$connected(package,$modpodpath)-$connected(version,$modpodpath)] - if {[file exists $unwrappedFolder]} { - #folder with exact version-match must exist for redirect to 'unwrapped' - set con(type,$modpodpath) "modpod-redirecting" - } - } - - } - set unwrapped_tm_file [file join $this_pkg_tm_folder] "[set connected(package,$modpodpath)]-[set connected(version,$modpodpath)].tm" - set connected(tmfile,$modpodpath) - set tail_segments [list] - set lcase_tmfile_segments [string tolower [file split $this_pkg_tm_folder]] - set lcase_modulepaths [string tolower [tcl::tm::list]] - foreach lc_mpath $lcase_modulepaths { - set mpath_segments [file split $lc_mpath] - if {[llength [struct::set intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { - set tail_segments [lrange [file split $this_pkg_tm_folder] [llength $mpath_segments] end] - break - } - } - if {[llength $tail_segments]} { - set connected(fullpackage,$modpodpath) [join [concat $tail_segments [set connected(package,$modpodpath)]] ::] ;#full name of package as used in package require - } else { - set connected(fullpackage,$modpodpath) [set connected(package,$modpodpath)] - } - - switch -exact -- $connected(type,$modpodpath) { - "modpod-redirecting" { - #redirect to the unwrapped version - set loadscript_name [file join $unwrappedFolder #modpod-loadscript-$con(package,$modpod).tcl] - - } - "unwrapped" { - if {[info commands ::thread::id] ne ""} { - set from [pid],[thread::id] - } else { - set from [pid] - } - #::modpod::Puts stderr "$from-> Package $connected(package,$modpodpath)-$connected(version,$modpodpath) is using unwrapped version: $modpodpath" - return [list ok ""] - } - default { - #autodetect .tm - zip/tar ? - #todo - use vfs ? - - #connect to tarball - start at 1st header - set connected(startdata,$modpodpath) 0 - set fh [open $modpodpath r] - set connected(fh,$modpodpath) $fh - fconfigure $fh -encoding iso8859-1 -translation binary -eofchar {} - - if {$connected(startdata,$modpodpath) >= 0} { - #verify we have a valid tar header - if {![catch {::modpod::system::tar::readHeader [red $fh 512]}]} { - seek $fh $connected(startdata,$modpodpath) start - return [list ok $fh] - } else { - #error "cannot verify tar header" - } - } - lpop connected(to) end - set connected(startdata,$modpodpath) -1 - unset connected(fh,$modpodpath) - catch {close $fh} - return [dict create err {Does not appear to be a valid modpod}] - } - } - } - proc disconnect {{modpod ""}} { - variable connected - if {![llength $connected(to)]} { - return 0 - } - if {$modpod eq ""} { - puts stderr "modpod::disconnect WARNING: modpod not explicitly specified. Disconnecting last connected: [lindex $connected(to) end]" - set modpod [lindex $connected(to) end] - } - - if {[set posn [lsearch $connected(to) $modpod]] == -1} { - puts stderr "modpod::disconnect WARNING: disconnect called when not connected: $modpod" - return 0 - } - if {[string length $connected(fh,$modpod)]} { - close $connected(fh,$modpod) - } - array unset connected *,$modpod - set connected(to) [lreplace $connected(to) $posn $posn] - return 1 - } - proc get {args} { - set argd [punk::args::get_dict { - -from -default "" -help "path to pod" - *values -min 1 -max 1 - filename - } $args] - set frompod [dict get $argd opts -from] - set filename [dict get $argd values filename] - - variable connected - #//review - set modpod [::modpod::system::connect_if_not $frompod] - set fh $connected(fh,$modpod) - if {$connected(type,$modpod) eq "unwrapped"} { - #for unwrapped connection - $connected(location) already points to the #modpod-pkg-ver folder - if {[string range $filename 0 0 eq "/"]} { - #absolute path (?) - set path [file join $connected(location,$modpod) .. [string trim $filename /]] - } else { - #relative path - use #modpod-xxx as base - set path [file join $connected(location,$modpod) $filename] - } - set fd [open $path r] - #utf-8? - #fconfigure $fd -encoding iso8859-1 -translation binary - return [list ok [lindex [list [read $fd] [close $fd]] 0]] - } else { - #read from vfs - puts stderr "get $filename from wrapped pod '$frompod' not implemented" - } - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace modpod ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -namespace eval modpod::lib { - namespace export {[a-z]*}; # Convention: export all lowercase - namespace path [namespace parent] - #*** !doctools - #[subsection {Namespace modpod::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] - # #[para]Description of utility1 - # return 1 - #} - - proc is_valid_tm_version {versionpart} { - #Needs to be suitable for use with Tcl's 'package vcompare' - if {![catch [list package vcompare $versionparts $versionparts]]} { - return 1 - } else { - return 0 - } - } - - #zipfile is a pure zip at this point - ie no script/exe header - proc make_zip_modpod {args} { - set argd [punk::args::get_dict { - @id -id ::modpod::lib::make_zip_modpod - -offsettype -default "archive" -choices {archive file} -help\ - "Whether zip offsets are relative to start of file or start of zip-data within the file. - 'archive' relative offsets are easier to work with (for writing/updating) in tools such as 7zip,peazip, - but other tools may be easier with 'file' relative offsets. (e.g info-zip,pkzip) - info-zip's 'zip -A' can sometimes convert archive-relative to file-relative. - -offsettype archive is equivalent to plain 'cat prefixfile zipfile > modulefile'" - @values -min 2 -max 2 - zipfile -type path -minsize 1 -help "path to plain zip file with subfolder #modpod-packagename-version containing .tm, data files and/or binaries" - outfile -type path -minsize 1 -help "path to output file. Name should be of the form packagename-version.tm" - } $args] - set zipfile [dict get $argd values zipfile] - set outfile [dict get $argd values outfile] - set opt_offsettype [dict get $argd opts -offsettype] - - - set mount_stub [string map [list %offsettype% $opt_offsettype] { - #zip file with Tcl loader prepended. Requires either builtin zipfs, or vfs::zip to mount while zipped. - #Alternatively unzip so that extracted #modpod-package-version folder is in same folder as .tm file. - #generated using: modpod::lib::make_zip_modpod -offsettype %offsettype% - if {[catch {file normalize [info script]} modfile]} { - error "modpod zip stub error. Unable to determine module path. (possible safe interp restrictions?)" - } - if {$modfile eq "" || ![file exists $modfile]} { - error "modpod zip stub error. Unable to determine module path" - } - set moddir [file dirname $modfile] - set mod_and_ver [file rootname [file tail $modfile]] - lassign [split $mod_and_ver -] moduletail version - if {[file exists $moddir/#modpod-$mod_and_ver]} { - source $moddir/#modpod-$mod_and_ver/$mod_and_ver.tm - } else { - #determine module namespace so we can mount appropriately - proc intersect {A B} { - if {[llength $A] == 0} {return {}} - if {[llength $B] == 0} {return {}} - if {[llength $B] > [llength $A]} { - set res $A - set A $B - set B $res - } - set res {} - foreach x $A {set ($x) {}} - foreach x $B { - if {[info exists ($x)]} { - lappend res $x - } - } - return $res - } - set lcase_tmfile_segments [string tolower [file split $moddir]] - set lcase_modulepaths [string tolower [tcl::tm::list]] - foreach lc_mpath $lcase_modulepaths { - set mpath_segments [file split $lc_mpath] - if {[llength [intersect $lcase_tmfile_segments $mpath_segments]] == [llength $mpath_segments]} { - set tail_segments [lrange [file split $moddir] [llength $mpath_segments] end] ;#use properly cased tail - break - } - } - if {[llength $tail_segments]} { - set fullpackage [join [concat $tail_segments $moduletail] ::] ;#full name of package as used in package require - set mount_at #modpod/[file join {*}$tail_segments]/#mounted-modpod-$mod_and_ver - } else { - set fullpackage $moduletail - set mount_at #modpod/#mounted-modpod-$mod_and_ver - } - - if {[info commands tcl::zipfs::mount] ne ""} { - #argument order changed to be consistent with vfs::zip::Mount etc - #early versions: zipfs::Mount mountpoint zipname - #since 2023-09: zipfs::Mount zipname mountpoint - #don't use 'file exists' when testing mountpoints. (some versions at least give massive delays on windows platform for non-existance) - #This is presumably related to // being interpreted as a network path - set mountpoints [dict keys [tcl::zipfs::mount]] - if {"//zipfs:/$mount_at" ni $mountpoints} { - #despite API change tcl::zipfs package version was unfortunately not updated - so we don't know argument order without trying it - if {[catch { - #tcl::zipfs::mount $modfile //zipfs:/#mounted-modpod-$mod_and_ver ;#extremely slow if this is a wrong guess (artifact of aforementioned file exists issue ?) - #puts "tcl::zipfs::mount $modfile $mount_at" - tcl::zipfs::mount $modfile $mount_at - } errM]} { - #try old api - if {![catch {tcl::zipfs::mount //zipfs:/$mount_at $modfile}]} { - puts stderr "modpod stub>>> tcl::zipfs::mount failed.\nbut old api: tcl::zipfs::mount succeeded\n tcl::zipfs::mount //zipfs://$mount_at $modfile" - puts stderr "Consider upgrading tcl runtime to one with fixed zipfs API" - } - } - if {![file exists //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { - puts stderr "modpod stub>>> mount at //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm failed\n zipfs mounts: [zipfs mount]" - #tcl::zipfs::unmount //zipfs:/$mount_at - error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" - } - } - # #modpod-$mod_and_ver subdirectory always present in the archive so it can be conveniently extracted and run in that form - source //zipfs:/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm - } else { - #fallback to slower vfs::zip - #NB. We don't create the intermediate dirs - but the mount still works - if {![file exists $moddir/$mount_at]} { - if {[catch {package require vfs::zip} errM]} { - set msg "Unable to load vfs::zip package to mount module $mod_and_ver (and zipfs not available either)" - append msg \n "If neither zipfs or vfs::zip are available - the module can still be loaded by manually unzipping the file $modfile in place." - append msg \n "The unzipped data will all be contained in a folder named #modpod-$mod_and_ver in the same parent folder as $modfile" - error $msg - } else { - set fd [vfs::zip::Mount $modfile $moddir/$mount_at] - if {![file exists $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm]} { - vfs::zip::Unmount $fd $moddir/$mount_at - error "Unable to find $mod_and_ver.tm in $modfile for module $fullpackage" - } - } - } - source $moddir/$mount_at/#modpod-$mod_and_ver/$mod_and_ver.tm - } - } - #zipped data follows - }] - #todo - test if supplied zipfile has #modpod-loadcript.tcl or some other script/executable before even creating? - append mount_stub \x1A - modpod::system::make_mountable_zip $zipfile $outfile $mount_stub $opt_offsettype - - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace modpod::lib ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -namespace eval modpod::system { - #*** !doctools - #[subsection {Namespace modpod::system}] - #[para] Internal functions that are not part of the API - - #deflate,store only supported - - #zipfile here is plain zip - no script/exe prefix part. - proc make_mountable_zip {zipfile outfile mount_stub {offsettype "archive"}} { - set inzip [open $zipfile r] - fconfigure $inzip -encoding iso8859-1 -translation binary - set out [open $outfile w+] - fconfigure $out -encoding iso8859-1 -translation binary - puts -nonewline $out $mount_stub - set stuboffset [tell $out] - lappend report "stub size: $stuboffset" - fcopy $inzip $out - close $inzip - - set size [tell $out] - lappend report "tmfile : [file tail $outfile]" - lappend report "output size : $size" - lappend report "offsettype : $offsettype" - - if {$offsettype eq "file"} { - #make zip offsets relative to start of whole file including prepended script. - #same offset structure as Tcl's 'zipfs mkimg' as at 2024-10 - #not editable by 7z,nanazip,peazip - - #we aren't adding any new files/folders so we can edit the offsets in place - - #Now seek in $out to find the end of directory signature: - #The structure itself is 24 bytes Long, followed by a maximum of 64Kbytes text - if {$size < 65559} { - set tailsearch_start 0 - } else { - set tailsearch_start [expr {$size - 65559}] - } - seek $out $tailsearch_start - set data [read $out] - #EOCD - End of Central Directory record - #PK\5\6 - set start_of_end [string last "\x50\x4b\x05\x06" $data] - #set start_of_end [expr {$start_of_end + $seek}] - #incr start_of_end $seek - set filerelative_eocd_posn [expr {$start_of_end + $tailsearch_start}] - - lappend report "kitfile-relative START-OF-EOCD: $filerelative_eocd_posn" - - seek $out $filerelative_eocd_posn - set end_of_ctrl_dir [read $out] - binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ - eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) - - lappend report "End of central directory: [array get eocd]" - seek $out [expr {$filerelative_eocd_posn+16}] - - #adjust offset of start of central directory by the length of our sfx stub - puts -nonewline $out [binary format i [expr {$eocd(diroffset) + $stuboffset}]] - flush $out - - seek $out $filerelative_eocd_posn - set end_of_ctrl_dir [read $out] - binary scan $end_of_ctrl_dir issssiis eocd(signature) eocd(disknbr) eocd(ctrldirdisk) \ - eocd(numondisk) eocd(totalnum) eocd(dirsize) eocd(diroffset) eocd(comment_len) - - # 0x06054b50 - end of central dir signature - puts stderr "$end_of_ctrl_dir" - puts stderr "comment_len: $eocd(comment_len)" - puts stderr "eocd sig: $eocd(signature) [punk::lib::dec2hex $eocd(signature)]" - lappend report "New dir offset: $eocd(diroffset)" - lappend report "Adjusting $eocd(totalnum) zip file items." - catch { - punk::lib::showdict -roottype list -chan stderr $report ;#heavy dependencies - } - - seek $out $eocd(diroffset) - for {set i 0} {$i <$eocd(totalnum)} {incr i} { - set current_file [tell $out] - set fileheader [read $out 46] - puts -------------- - puts [ansistring VIEW -lf 1 $fileheader] - puts -------------- - #binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ - # x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) - - binary scan $fileheader ic4sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ - x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) - set ::last_header $fileheader - - puts "sig: $x(sig) (hex: [punk::lib::dec2hex $x(sig)])" - puts "ver: $x(version)" - puts "method: $x(method)" - - #PK\1\2 - #33639248 dec = 0x02014b50 - central directory file header signature - if { $x(sig) != 33639248 } { - error "modpod::system::make_mountable_zip Bad file header signature at item $i: dec:$x(sig) hex:[punk::lib::dec2hex $x(sig)]" - } - - foreach size $x(lengths) var {filename extrafield comment} { - if { $size > 0 } { - set x($var) [read $out $size] - } else { - set x($var) "" - } - } - set next_file [tell $out] - lappend report "file $i: $x(offset) $x(sizes) $x(filename)" - - seek $out [expr {$current_file+42}] - puts -nonewline $out [binary format i [expr {$x(offset)+$stuboffset}]] - - #verify: - flush $out - seek $out $current_file - set fileheader [read $out 46] - lappend report "old $x(offset) + $stuboffset" - binary scan $fileheader is2sss2ii2s3ssii x(sig) x(version) x(flags) x(method) \ - x(date) x(crc32) x(sizes) x(lengths) x(diskno) x(iattr) x(eattr) x(offset) - lappend report "new $x(offset)" - - seek $out $next_file - } - } - - close $out - #pdict/showdict reuire punk & textlib - ie lots of dependencies - #don't fall over just because of that - catch { - punk::lib::showdict -roottype list -chan stderr $report - } - #puts [join $report \n] - return - } - - proc connect_if_not {{podpath ""}} { - upvar ::modpod::connected connected - set podpath [::modpod::system::normalize $podpath] - set docon 0 - if {![llength $connected(to)]} { - if {![string length $podpath]} { - error "modpod::system::connect_if_not - Not connected to a modpod file, and no podpath specified" - } else { - set docon 1 - } - } else { - if {![string length $podpath]} { - set podpath [lindex $connected(to) end] - puts stderr "modpod::system::connect_if_not WARNING: using last connected modpod:$podpath for operation\n -podpath not explicitly specified during operation: [info level -1]" - } else { - if {$podpath ni $connected(to)} { - set docon 1 - } - } - } - if {$docon} { - if {[lindex [modpod::connect $podpath]] 0] ne "ok"} { - error "modpod::system::connect_if_not error. file $podpath does not seem to be a valid modpod" - } else { - return $podpath - } - } - #we were already connected - return $podpath - } - - proc myversion {} { - upvar ::modpod::connected connected - set script [info script] - if {![string length $script]} { - error "No result from \[info script\] - modpod::system::myversion should only be called from within a loading modpod" - } - set fname [file tail [file rootname [file normalize $script]]] - set scriptdir [file dirname $script] - - if {![string match "#modpod-*" $fname]} { - lassign [lrange [split $fname -] end-1 end] _pkgname version - } else { - lassign [scan [file tail [file rootname $script]] {#modpod-loadscript-%[a-z]-%s}] _pkgname version - if {![string length $version]} { - #try again on the name of the containing folder - lassign [scan [file tail $scriptdir] {#modpod-%[a-z]-%s}] _pkgname version - #todo - proper walk up the directory tree - if {![string length $version]} { - #try again on the grandparent folder (this is a standard depth for sourced .tcl files in a modpod) - lassign [scan [file tail [file dirname $scriptdir]] {#modpod-%[a-z]-%s}] _pkgname version - } - } - } - - #tarjar::Log debug "'myversion' determined version for [info script]: $version" - return $version - } - - proc myname {} { - upvar ::modpod::connected connected - set script [info script] - if {![string length $script]} { - error "No result from \[info script\] - modpod::system::myname should only be called from within a loading modpod" - } - return $connected(fullpackage,$script) - } - proc myfullname {} { - upvar ::modpod::connected connected - set script [info script] - #set script [::tarjar::normalize $script] - set script [file normalize $script] - if {![string length $script]} { - error "No result from \[info script\] - modpod::system::myfullname should only be called from within a loading tarjar" - } - return $::tarjar::connected(fullpackage,$script) - } - proc normalize {path} { - #newer versions of Tcl don't do tilde sub - - #Tcl's 'file normalize' seems to do some unfortunate tilde substitution on windows.. (at least for relative paths) - # we take the assumption here that if Tcl's tilde substitution is required - it should be done before the path is provided to this function. - set matilda "<_tarjar_tilde_placeholder_>" ;#token that is *unlikely* to occur in the wild, and is somewhat self describing in case it somehow ..escapes.. - set path [string map [list ~ $matilda] $path] ;#give our tildes to matilda to look after - set path [file normalize $path] - #set path [string tolower $path] ;#must do this after file normalize - return [string map [list $matilda ~] $path] ;#get our tildes back. -} -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide modpod [namespace eval modpod { - variable pkg modpod - variable version - set version 0.1.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/overtype-1.6.5.tm b/src/bootsupport/modules/overtype-1.6.5.tm deleted file mode 100644 index 9363fb6d..00000000 --- a/src/bootsupport/modules/overtype-1.6.5.tm +++ /dev/null @@ -1,4773 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from -buildversion.txt -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) Julian Noble 2003-2023 -# -# @@ Meta Begin -# Application overtype 1.6.5 -# Meta platform tcl -# Meta license BSD -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin overtype_module_overtype 0 1.6.5] -#[copyright "2024"] -#[titledesc {overtype text layout - ansi aware}] [comment {-- Name section and table of contents description --}] -#[moddesc {overtype text layout}] [comment {-- Description at end of page heading --}] -#[require overtype] -#[keywords module text ansi] -#[description] -#[para] - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] overview of overtype -#[subsection Concepts] -#[para] - - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[subsection dependencies] -#[para] packages used by overtype -#[list_begin itemized] - -package require Tcl 8.6- -package require textutil -package require punk::lib ;#required for lines_as_list -package require punk::ansi ;#required to detect, split, strip and calculate lengths -package require punk::char ;#box drawing - and also unicode character width determination for proper layout of text with double-column-width chars -package require punk::assertion -#*** !doctools -#[item] [package {Tcl 8.6}] -#[item] [package textutil] -#[item] [package punk::ansi] -#[para] - required to detect, split, strip and calculate lengths of text possibly containing ansi codes -#[item] [package punk::char] -#[para] - box drawing - and also unicode character width determination for proper layout of text with double-column-width chars - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -#PERFORMANCE notes -#overtype is very performance sensitive - used in ansi output all over the place ie needs to be optimised -#NOTE use of tcl::dict::for tcl::string::range etc instead of ensemble versions. This is for the many tcl 8.6/8.7 interps which don't compile ensemble commands when in safe interps -#similar for tcl::namespace::eval - but this is at least on some versions of Tcl - faster even in a normal interp. Review to see if that holds for Tcl 9. -#for string map: when there are exactly 2 elements - it is faster to use a literal which has a special case optimisation in the c code -#ie use tcl::string::map {\n ""} ... instead of tcl::string::map [list \n ""] ... -#note that we can use unicode (e.g \uFF31) and other escapes such as \t within these curly braces - we don't have to use double quotes -#generally using 'list' is preferred for the map as less error prone. -#can also use: tcl::string::map "token $var" .. but be careful regarding quoting and whitespace in var. This should be used sparingly if at all. - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section API] - - -#Julian Noble - 2003 -#Released under standard 'BSD license' conditions. -# -#todo - ellipsis truncation indicator for center,right - -#v1.4 2023-07 - naive ansi color handling - todo - fix tcl::string::range -# - need to extract and replace ansi codes? - -tcl::namespace::eval overtype { - namespace import ::punk::assertion::assert - punk::assertion::active true - - namespace path ::punk::lib - - namespace export * - variable default_ellipsis_horizontal "..." ;#fallback - variable default_ellipsis_vertical "..." - tcl::namespace::eval priv { - proc _init {} { - upvar ::overtype::default_ellipsis_horizontal e_h - upvar ::overtype::default_ellipsis_vertical e_v - set e_h [format %c 0x2026] ;#Unicode Horizontal Ellipsis - set e_v [format %c 0x22EE] - #The unicode ellipsis looks more natural than triple-dash which is centred vertically whereas ellipsis is at floorline of text - #Also - unicode ellipsis has semantic meaning that other processors can interpret - #unicode does also provide a midline horizontal ellipsis 0x22EF - - #set e [format %c 0x2504] ;#punk::char::charshort boxd_ltdshhz - Box Drawings Light Triple Dash Horizontal - #if {![catch {package require punk::char}]} { - # set e [punk::char::charshort boxd_ltdshhz] - #} - } - } - priv::_init -} -proc overtype::about {} { - return "Simple text formatting. Author JMN. BSD-License" -} - -tcl::namespace::eval overtype { - variable grapheme_widths [tcl::dict::create] - - variable escape_terminals - #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). - tcl::dict::set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] - #tcl::dict::set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic - tcl::dict::set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals - - #self-contained 2 byte ansi escape sequences - review more? - variable ansi_2byte_codes_dict - set ansi_2byte_codes_dict [tcl::dict::create\ - "reset_terminal" "\u001bc"\ - "save_cursor_posn" "\u001b7"\ - "restore_cursor_posn" "\u001b8"\ - "cursor_up_one" "\u001bM"\ - "NEL - Next Line" "\u001bE"\ - "IND - Down one line" "\u001bD"\ - "HTS - Set Tab Stop" "\u001bH"\ - ] - - #debatable whether strip should reveal the somethinghidden - some terminals don't hide it anyway. - # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ -} - - - - -proc overtype::string_columns {text} { - if {[punk::ansi::ta::detect $text]} { - #error "error string_columns is for calculating character length of string - ansi codes must be stripped/rendered first e.g with punk::ansi::ansistrip. Alternatively try punk::ansi::printing_length" - set text [punk::ansi::ansistrip $text] - } - return [punk::char::ansifreestring_width $text] -} - -#todo - consider a way to merge overtype::left/centre/right -#These have similar algorithms/requirements - and should be refactored to be argument-wrappers over a function called something like overtype::renderblock -#overtype::renderblock could render the input to a defined (possibly overflowing in x or y) rectangle overlapping the underlay. -#(i.e not even necessariy having it's top left within the underlay) -tcl::namespace::eval overtype::priv { -} - -#could return larger than renderwidth -proc _get_row_append_column {row} { - #obsolete? - upvar outputlines outputlines - set idx [expr {$row -1}] - if {$row <= 1 || $row > [llength $outputlines]} { - return 1 - } else { - upvar opt_expand_right expand_right - upvar renderwidth renderwidth - set existinglen [punk::ansi::printing_length [lindex $outputlines $idx]] - set endpos [expr {$existinglen +1}] - if {$expand_right} { - return $endpos - } else { - if {$endpos > $renderwidth} { - return $renderwidth + 1 - } else { - return $endpos - } - } - } -} - -tcl::namespace::eval overtype { - #*** !doctools - #[subsection {Namespace overtype}] - #[para] Core API functions for overtype - #[list_begin definitions] - - - - #tcl::string::range should generally be avoided for both undertext and overtext which contain ansi escapes and other cursor affecting chars such as \b and \r - #render onto an already-rendered (ansi already processed) 'underlay' string, a possibly ansi-laden 'overlay' string. - #The underlay and overlay can be multiline blocks of text of varying line lengths. - #The overlay may just be an ansi-colourised block - or may contain ansi cursor movements and cursor save/restore calls - in which case the apparent length and width of the overlay can't be determined as if it was a block of text. - #This is a single-shot rendering of strings - ie there is no way to chain another call containing a cursor-restore to previously rendered output and have it know about any cursor-saves in the first call. - # a cursor start position other than top-left is a possible addition to consider. - #see editbuf in punk::repl for a more stateful ansi-processor. Both systems use loops over overtype::renderline - proc renderspace {args} { - #*** !doctools - #[call [fun overtype::renderspace] [arg args] ] - #[para] usage: ?-transparent [lb]0|1[rb]? ?-expand_right [lb]1|0[rb]? ?-ellipsis [lb]1|0[rb]? ?-ellipsistext ...? undertext overtext - - # @c overtype starting at left (overstrike) - # @c can/should we use something like this?: 'format "%-*s" $len $overtext - variable default_ellipsis_horizontal - - if {[llength $args] < 2} { - error {usage: ?-width ? ?-startcolumn ? ?-transparent [0|1|]? ?-expand_right [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} - } - set optargs [lrange $args 0 end-2] - if {[llength $optargs] % 2 == 0} { - set overblock [lindex $args end] - set underblock [lindex $args end-1] - #lassign [lrange $args end-1 end] underblock overblock - set argsflags [lrange $args 0 end-2] - } else { - set optargs [lrange $args 0 end-1] - if {[llength $optargs] %2 == 0} { - set overblock [lindex $args end] - set underblock "" - set argsflags [lrange $args 0 end-1] - } else { - error "renderspace expects opt-val pairs followed by: or just " - } - } - set opts [tcl::dict::create\ - -bias ignored\ - -width \uFFEF\ - -height \uFFEF\ - -startcolumn 1\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -expand_right 0\ - -appendlines 1\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -experimental 0\ - -cp437 0\ - -looplimit \uFFEF\ - -crm_mode 0\ - -reverse_mode 0\ - -insert_mode 0\ - -wrap 0\ - -info 0\ - -console {stdin stdout stderr}\ - ] - #expand_right is perhaps consistent with the idea of the page_size being allowed to grow horizontally.. - # it does not necessarily mean the viewport grows. (which further implies need for horizontal scrolling) - # - it does need to be within some concept of terminal width - as columns must be addressable by ansi sequences. - # - This implies the -width option value must grow if it is tied to the concept of renderspace terminal width! REVIEW. - # - further implication is that if expand_right grows the virtual renderspace terminal width - - # then some sort of reflow/rerender needs to be done for preceeding lines? - # possibly not - as expand_right is distinct from a normal terminal-width change event, - # expand_right being primarily to support other operations such as textblock::table - - #todo - viewport width/height as separate concept to terminal width/height? - #-ellipsis args not used if -wrap is true - foreach {k v} $argsflags { - switch -- $k { - -looplimit - -width - -height - -startcolumn - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - - -transparent - -exposed1 - -exposed2 - -experimental - - -expand_right - -appendlines - - -reverse_mode - -crm_mode - -insert_mode - - -cp437 - - -info - -console { - tcl::dict::set opts $k $v - } - -wrap - -autowrap_mode { - #temp alias -autowrap_mode for consistency with renderline - #todo - - tcl::dict::set opts -wrap $v - } - default { - error "overtype::renderspace unknown option '$k'. Known options: [tcl::dict::keys $opts]" - } - } - } - #set opts [tcl::dict::merge $defaults $argsflags] - # -- --- --- --- --- --- - #review - expand_left for RTL text? - set opt_expand_right [tcl::dict::get $opts -expand_right] - #for repl - standard output line indicator is a dash - todo, add a different indicator for a continued line. - set opt_width [tcl::dict::get $opts -width] - set opt_height [tcl::dict::get $opts -height] - set opt_startcolumn [tcl::dict::get $opts -startcolumn] - set opt_appendlines [tcl::dict::get $opts -appendlines] - set opt_transparent [tcl::dict::get $opts -transparent] - set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] - set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] - set opt_exposed1 [tcl::dict::get $opts -exposed1] ;#widechar_exposed_left - todo - set opt_exposed2 [tcl::dict::get $opts -exposed2] ;#widechar_exposed_right - todo - # -- --- --- --- --- --- - set opt_crm_mode [tcl::dict::get $opts -crm_mode] - set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] - set opt_insert_mode [tcl::dict::get $opts -insert_mode] - ##### - # review -wrap should map onto DECAWM terminal mode - the wrap 2 idea may not fit in with this?. - set opt_autowrap_mode [tcl::dict::get $opts -wrap] - #??? wrap 1 is hard wrap cutting word at exact column, or 1 column early for 2w-glyph, wrap 2 is for language-based word-wrap algorithm (todo) - ##### - # -- --- --- --- --- --- - set opt_cp437 [tcl::dict::get $opts -cp437] - set opt_info [tcl::dict::get $opts -info] - - - - # ---------------------------- - # -experimental dev flag to set flags etc - # ---------------------------- - set data_mode 0 - set edit_mode 0 - set opt_experimental [tcl::dict::get $opts -experimental] - foreach o $opt_experimental { - switch -- $o { - data_mode { - set data_mode 1 - } - edit_mode { - set edit_mode 1 - } - } - } - # ---------------------------- - - - set underblock [tcl::string::map {\r\n \n} $underblock] - set overblock [tcl::string::map {\r\n \n} $overblock] - - - #set underlines [split $underblock \n] - - #underblock is a 'rendered' block - so width height make sense - #only non-cursor affecting and non-width occupying ANSI codes should be present. - #ie SGR codes and perhaps things such as PM - although generally those should have been pushed to the application already - #renderwidth & renderheight were originally used with reference to rendering into a 'column' of output e.g a table column - before cursor row/col was implemented. - - if {$opt_width eq "\uFFEF" || $opt_height eq "\uFFEF"} { - lassign [blocksize $underblock] _w renderwidth _h renderheight - if {$opt_width ne "\uFFEF"} { - set renderwidth $opt_width - } - if {$opt_height ne "\uFFEF"} { - set renderheight $opt_height - } - } else { - set renderwidth $opt_width - set renderheight $opt_height - } - #initial state for renderspace 'terminal' reset - set initial_state [dict create\ - renderwidth $renderwidth\ - renderheight $renderheight\ - crm_mode $opt_crm_mode\ - reverse_mode $opt_reverse_mode\ - insert_mode $opt_insert_mode\ - autowrap_mode $opt_autowrap_mode\ - cp437 $opt_cp437\ - ] - #modes - #e.g insert_mode can be toggled by insert key or ansi IRM sequence CSI 4 h|l - #opt_startcolumn ?? - DECSLRM ? - set vtstate $initial_state - - # -- --- --- --- - #REVIEW - do we need ansi resets in the underblock? - if {$underblock eq ""} { - set underlines [lrepeat $renderheight ""] - } else { - set underblock [textblock::join_basic -- $underblock] ;#ensure properly rendered - ansi per-line resets & replays - set underlines [split $underblock \n] - } - #if {$underblock eq ""} { - # set blank "\x1b\[0m\x1b\[0m" - # #set underlines [list "\x1b\[0m\x1b\[0m"] - # set underlines [lrepeat $renderheight $blank] - #} else { - # #lines_as_list -ansiresets 1 will do nothing if -ansiresets 1 isn't specified - REVIEW - # set underlines [lines_as_list -ansiresets 1 $underblock] - #} - # -- --- --- --- - - #todo - reconsider the 'line' as the natural chunking mechanism for the overlay. - #In practice an overlay ANSI stream can be a single line with ansi moves/restores etc - or even have no moves or newlines, just relying on wrapping at the output renderwidth - #In such cases - we process the whole shebazzle for the first output line - only reducing by the applied amount at the head each time, reprocessing the long tail each time. - #(in cases where there are interline moves or cursor jumps anyway) - #This works - but doesn't seem efficient. - #On the other hand.. maybe it depends on the data. For simpler files it's more efficient than splitting first - - #a hack until we work out how to avoid infinite loops... - # - set looplimit [tcl::dict::get $opts -looplimit] - if {$looplimit eq "\uFFEF"} { - #looping for each char is worst case (all newlines?) - anything over that is an indication of something broken? - #do we need any margin above the length? (telnet mapscii.me test) - set looplimit [expr {[tcl::string::length $overblock] + 10}] - } - - #overblock height/width isn't useful in the presence of an ansi input overlay with movements. The number of lines may bear little relationship to the output height - #lassign [blocksize $overblock] _w overblock_width _h overblock_height - - set scheme 4 - switch -- $scheme { - 0 { - #one big chunk - set inputchunks [list $overblock] - } - 1 { - set inputchunks [punk::ansi::ta::split_codes $overblock] - } - 2 { - - #split into lines if possible first - then into plaintext/ansi-sequence chunks ? - set inputchunks [list ""] ;#put an empty plaintext split in for starters - set i 1 - set lines [split $overblock \n] - foreach ln $lines { - if {$i < [llength $lines]} { - append ln \n - } - set sequence_split [punk::ansi::ta::split_codes_single $ln] ;#use split_codes Not split_codes_single? - set lastpt [lindex $inputchunks end] - lset inputchunks end [tcl::string::cat $lastpt [lindex $sequence_split 0]] - lappend inputchunks {*}[lrange $sequence_split 1 end] - incr i - } - } - 3 { - #it turns out line based chunks are faster than the above.. probably because some of those end up doing the regex splitting twice - set lflines [list] - set inputchunks [split $overblock \n] - foreach ln $inputchunks { - append ln \n - lappend lflines $ln - } - if {[llength $lflines]} { - lset lflines end [tcl::string::range [lindex $lflines end] 0 end-1] - } - #set inputchunks $lflines[unset lflines] - set inputchunks [lindex [list $lflines [unset lflines]] 0] - - } - 4 { - set inputchunks [list] - foreach ln [split $overblock \n] { - lappend inputchunks $ln\n - } - if {[llength $inputchunks]} { - lset inputchunks end [tcl::string::range [lindex $inputchunks end] 0 end-1] - } - } - } - - - - - set replay_codes_underlay [tcl::dict::create 1 ""] - #lappend replay_codes_overlay "" - set replay_codes_overlay "[punk::ansi::a]" - set unapplied "" - set cursor_saved_position [tcl::dict::create] - set cursor_saved_attributes "" - - - set outputlines $underlines - set overidx 0 - - #underlines are not necessarily processed in order - depending on cursor-moves applied from overtext - set row 1 - #if {$data_mode} { - # set col [_get_row_append_column $row] - #} else { - set col $opt_startcolumn - #} - - set instruction_stats [tcl::dict::create] - - set loop 0 - #while {$overidx < [llength $inputchunks]} { } - - while {[llength $inputchunks]} { - #set overtext [lindex $inputchunks $overidx]; lset inputchunks $overidx "" - set overtext [lpop inputchunks 0] - if {![tcl::string::length $overtext]} { - incr loop - continue - } - #puts "----->[ansistring VIEW -lf 1 -vt 1 -nul 1 $overtext]<----" - set undertext [lindex $outputlines [expr {$row -1}]] - set renderedrow $row - - #renderline pads each underaly line to width with spaces and should track where end of data is - - - #set overtext [tcl::string::cat [lindex $replay_codes_overlay $overidx] $overtext] - set overtext $replay_codes_overlay$overtext - if {[tcl::dict::exists $replay_codes_underlay $row]} { - set undertext [tcl::dict::get $replay_codes_underlay $row]$undertext - } - #review insert_mode. As an 'overtype' function whose main function is not interactive keystrokes - insert is secondary - - #but even if we didn't want it as an option to the function call - to process ansi adequately we need to support IRM (insertion-replacement mode) ESC [ 4 h|l - set renderopts [list -experimental $opt_experimental\ - -cp437 $opt_cp437\ - -info 1\ - -crm_mode [tcl::dict::get $vtstate crm_mode]\ - -insert_mode [tcl::dict::get $vtstate insert_mode]\ - -autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\ - -reverse_mode [tcl::dict::get $vtstate reverse_mode]\ - -cursor_restore_attributes $cursor_saved_attributes\ - -transparent $opt_transparent\ - -width [tcl::dict::get $vtstate renderwidth]\ - -exposed1 $opt_exposed1\ - -exposed2 $opt_exposed2\ - -expand_right $opt_expand_right\ - -cursor_column $col\ - -cursor_row $row\ - ] - set rinfo [renderline {*}$renderopts $undertext $overtext] - - set instruction [tcl::dict::get $rinfo instruction] - tcl::dict::set vtstate crm_mode [tcl::dict::get $rinfo crm_mode] - tcl::dict::set vtstate insert_mode [tcl::dict::get $rinfo insert_mode] - tcl::dict::set vtstate autowrap_mode [tcl::dict::get $rinfo autowrap_mode] ;# - tcl::dict::set vtstate reverse_mode [tcl::dict::get $rinfo reverse_mode] - #how to support reverse_mode in rendered linelist? we need to examine all pt/code blocks and flip each SGR stack? - # - review - the answer is probably that we don't need to - it is set/reset only during application of overtext - - #Note carefully the difference betw overflow_right and unapplied. - #overflow_right may need to be included in next run before the unapplied data - #overflow_right most commonly has data when in insert_mode - set rendered [tcl::dict::get $rinfo result] - set overflow_right [tcl::dict::get $rinfo overflow_right] - set overflow_right_column [tcl::dict::get $rinfo overflow_right_column] - set unapplied [tcl::dict::get $rinfo unapplied] - set unapplied_list [tcl::dict::get $rinfo unapplied_list] - set post_render_col [tcl::dict::get $rinfo cursor_column] - set post_render_row [tcl::dict::get $rinfo cursor_row] - set c_saved_pos [tcl::dict::get $rinfo cursor_saved_position] - set c_saved_attributes [tcl::dict::get $rinfo cursor_saved_attributes] - set visualwidth [tcl::dict::get $rinfo visualwidth] ;#column width of what is 'rendered' for the line - set insert_lines_above [tcl::dict::get $rinfo insert_lines_above] - set insert_lines_below [tcl::dict::get $rinfo insert_lines_below] - tcl::dict::set replay_codes_underlay [expr {$renderedrow+1}] [tcl::dict::get $rinfo replay_codes_underlay] - - #lset replay_codes_overlay [expr $overidx+1] [tcl::dict::get $rinfo replay_codes_overlay] - set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] - if {0 && [tcl::dict::get $vtstate reverse_mode]} { - #test branch - todo - prune - puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered" - #review - #JMN3 - set existing_reverse_state 0 - #split_codes_single is single esc sequence - but could have multiple sgr codes within one esc sequence - #e.g \x1b\[0;31;7m has a reset,colour red and reverse - set codeinfo [punk::ansi::codetype::sgr_merge [list $replay_codes_overlay] -info 1] - set codestate_reverse [dict get $codeinfo codestate reverse] - switch -- $codestate_reverse { - 7 { - set existing_reverse_state 1 - } - 27 { - set existing_reverse_state 0 - } - "" { - } - } - if {$existing_reverse_state == 0} { - set rflip [a+ reverse] - } else { - #reverse of reverse - set rflip [a+ noreverse] - } - #note that mergeresult can have multiple esc (due to unmergeables or non sgr codes) - set replay_codes_overlay [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]] - puts stderr "---->[ansistring VIEW $replay_codes_overlay] rendered: $rendered" - } - - - - #-- todo - detect looping properly - if {$row > 1 && $overtext ne "" && $unapplied eq $overtext && $post_render_row == $row && $instruction eq ""} { - puts stderr "overtype::renderspace loop?" - puts [ansistring VIEW $rinfo] - break - } - #-- - - if {[tcl::dict::size $c_saved_pos] >= 1} { - set cursor_saved_position $c_saved_pos - set cursor_saved_attributes $c_saved_attributes - } - - - set overflow_handled 0 - - - - set nextprefix "" - - - #todo - handle potential insertion mode as above for cursor restore? - #keeping separate branches for debugging - review and merge as appropriate when stable - set instruction_type [lindex $instruction 0] ;#some instructions have params - tcl::dict::incr instruction_stats $instruction_type - switch -- $instruction_type { - reset { - #reset the 'renderspace terminal' (not underlying terminal) - set row 1 - set col 1 - set vtstate [tcl::dict::merge $vtstate $initial_state] - #todo - clear screen - } - {} { - #end of supplied line input - #lf included in data - set row $post_render_row - set col $post_render_col - if {![llength $unapplied_list]} { - if {$overflow_right ne ""} { - incr row - } - } else { - puts stderr "renderspace end of input line - has unapplied: [ansistring VIEW $unapplied] (review)" - } - set col $opt_startcolumn - } - up { - - #renderline knows it's own line number, and knows not to go above row l - #it knows that a move whilst 1-beyond the width conflicts with the linefeed and reduces the move by one accordingly. - #row returned should be correct. - #column may be the overflow column - as it likes to report that to the caller. - - #Note that an ansi up sequence after last column going up to a previous line and also beyond the last column, will result in the next grapheme going onto the following line. - #this seems correct - as the column remains beyond the right margin so subsequent chars wrap (?) review - #puts stderr "up $post_render_row" - #puts stderr "$rinfo" - - #puts stdout "1 row:$row col $col" - set row $post_render_row - #data_mode (naming?) determines if we move to end of existing data or not. - #data_mode 0 means ignore existing line length and go to exact column - #set by -experimental flag - if {$data_mode == 0} { - set col $post_render_col - } else { - #This doesn't really work if columns are pre-filled with spaces..we can't distinguish them from data - #we need renderline to return the number of the maximum column filled (or min if we ever do r-to-l) - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $renderwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $renderwidth - } - } - - #puts stdout "2 row:$row col $col" - #puts stdout "-----------------------" - #puts stdout $rinfo - #flush stdout - } - down { - if {$data_mode == 0} { - #renderline doesn't know how far down we can go.. - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set row $post_render_row - set col $post_render_col - } else { - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $renderwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $renderwidth - } - - } - } - restore_cursor { - #testfile belinda.ans uses this - - #puts stdout "[a+ blue bold]CURSOR_RESTORE[a]" - if {[tcl::dict::exists $cursor_saved_position row]} { - set row [tcl::dict::get $cursor_saved_position row] - set col [tcl::dict::get $cursor_saved_position column] - #puts stdout "restoring: row $row col $col [ansistring VIEW $cursor_saved_attributes] [a] unapplied [ansistring VIEWCODES $unapplied]" - #set nextprefix $cursor_saved_attributes - #lset replay_codes_overlay [expr $overidx+1] $cursor_saved_attributes - set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay]$cursor_saved_attributes - #set replay_codes_overlay $cursor_saved_attributes - set cursor_saved_position [tcl::dict::create] - set cursor_saved_attributes "" - } else { - #TODO - #?restore without save? - #should move to home position and reset ansi SGR? - #puts stderr "overtype::renderspace cursor_restore without save data available" - } - #If we were inserting prior to hitting the cursor_restore - there could be overflow_right data - generally the overtype functions aren't for inserting - but ansi can enable it - #if we were already in overflow when cursor_restore was hit - it shouldn't have been processed as an action - just stored. - if {!$overflow_handled && $overflow_right ne ""} { - #wrap before restore? - possible effect on saved cursor position - #this overflow data has previously been rendered so has no cursor movements or further save/restore operations etc - #we can just insert another call to renderline to solve this.. ? - #It would perhaps be more properly handled as a queue of instructions from our initial renderline call - #we don't need to worry about overflow next call (?)- but we should carry forward our gx and ansi stacks - - puts stdout ">>>[a+ red bold]overflow_right during restore_cursor[a]" - - set sub_info [overtype::renderline\ - -info 1\ - -width [tcl::dict::get $vtstate renderwidth]\ - -insert_mode [tcl::dict::get $vtstate insert_mode]\ - -autowrap_mode [tcl::dict::get $vtstate autowrap_mode]\ - -expand_right [tcl::dict::get $opts -expand_right]\ - ""\ - $overflow_right\ - ] - set foldline [tcl::dict::get $sub_info result] - tcl::dict::set vtstate insert_mode [tcl::dict::get $sub_info insert_mode] ;#probably not needed..? - tcl::dict::set vtstate autowrap_mode [tcl::dict::get $sub_info autowrap_mode] ;#nor this.. - linsert outputlines $renderedrow $foldline - #review - row & col set by restore - but not if there was no save.. - } - set overflow_handled 1 - - } - move { - ######## - if {$post_render_row > [llength $outputlines]} { - #Ansi moves need to create new lines ? - #if {$opt_appendlines} { - # set diff [expr {$post_render_row - [llength $outputlines]}] - # if {$diff > 0} { - # lappend outputlines {*}[lrepeat $diff ""] - # } - # set row $post_render_row - #} else { - set row [llength $outputlines] - #} - } else { - set row $post_render_row - } - ####### - set col $post_render_col - #overflow + unapplied? - } - clear_and_move { - #e.g 2J - if {$post_render_row > [llength $outputlines]} { - set row [llength $outputlines] - } else { - set row $post_render_row - } - set col $post_render_col - set overflow_right "" ;#if we're clearing - any overflow due to insert_mode is irrelevant - set clearedlines [list] - foreach ln $outputlines { - lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $renderwidth]\x1b\[0m - if 0 { - - set lineparts [punk::ansi::ta::split_codes $ln] - set numcells 0 - foreach {pt _code} $lineparts { - if {$pt ne ""} { - foreach grapheme [punk::char::grapheme_split $pt] { - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - incr numcells 1 - } - default { - if {$grapheme eq "\u0000"} { - incr numcells 1 - } else { - incr numcells [grapheme_width_cached $grapheme] - } - } - } - - } - } - } - #replays/resets each line - lappend clearedlines \x1b\[0m$replay_codes_overlay[string repeat \000 $numcells]\x1b\[0m - } - } - set outputlines $clearedlines - #todo - determine background/default to be in effect - DECECM ? - puts stderr "replay_codes_overlay: [ansistring VIEW $replay_codes_overlay]" - #lset outputlines 0 $replay_codes_overlay[lindex $outputlines 0] - - } - lf_start { - #raw newlines - # ---------------------- - #test with fruit.ans - #test - treating as newline below... - #append rendered $overflow_right - #set overflow_right "" - set row $renderedrow - incr row - if {$row > [llength $outputlines]} { - lappend outputlines "" - } - set col $opt_startcolumn - # ---------------------- - } - lf_mid { - - set edit_mode 0 - if {$edit_mode} { - set inputchunks [linsert $inputchunks 0 $overflow_right$unapplied] - set overflow_right "" - set unapplied "" - set row $post_render_row - #set col $post_render_col - set col $opt_startcolumn - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - if 1 { - if {$overflow_right ne ""} { - if {$opt_expand_right} { - append rendered $overflow_right - set overflow_right "" - } else { - #review - we should really make renderline do the work...? - set overflow_width [punk::ansi::printing_length $overflow_right] - if {$visualwidth + $overflow_width <= $renderwidth} { - append rendered $overflow_right - set overflow_right "" - } else { - if {[tcl::dict::get $vtstate autowrap_mode]} { - set outputlines [linsert $outputlines $renderedrow $overflow_right] - set overflow_right "" - set row [expr {$renderedrow + 2}] - } else { - set overflow_right "" ;#abandon - } - - if {0 && $visualwidth < $renderwidth} { - puts stderr "visualwidth: $visualwidth < renderwidth:$renderwidth" - error "incomplete - abandon?" - set overflowparts [punk::ansi::ta::split_codes $overflow_right] - set remaining_overflow $overflowparts - set filled 0 - foreach {pt code} $overflowparts { - lpop remaining_overflow 0 - if {$pt ne ""} { - set graphemes [punk::char::grapheme_split $pt] - set add "" - set addlen $visualwidth - foreach g $graphemes { - set w [overtype::grapheme_width_cached $g] - if {$addlen + $w <= $renderwidth} { - append add $g - incr addlen $w - } else { - set filled 1 - break - } - } - append rendered $add - } - if {!$filled} { - lpop remaining_overflow 0 ;#pop code - } - } - set overflow_right [join $remaining_overflow ""] - } - } - } - } - set row $post_render_row - set col $opt_startcolumn - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } else { - #old version - known to work with various ansi graphics - e.g fruit.ans - # - but fails to limit lines to renderwidth when expand_right == 0 - append rendered $overflow_right - set overflow_right "" - set row $post_render_row - set col $opt_startcolumn - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - } - } - } - lf_overflow { - #linefeed after renderwidth e.g at column 81 for an 80 col width - #we may also have other control sequences that came after col 80 e.g cursor save - - if 0 { - set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -nul 1 -lf 1 -vt 1 $rendered]] - set lhs [textblock::frame -title "rendered $visualwidth cols" -subtitle "row-$renderedrow" $lhs] - set rhs "" - - #assertion - there should be no overflow.. - puts $lhs - } - if {![tcl::dict::get $vtstate insert_mode]} { - assert {$overflow_right eq ""} lf_overflow should not get data in overflow_right when not insert_mode - } - - set row $post_render_row - #set row $renderedrow - #incr row - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat 1 ""] - } - set col $opt_startcolumn - - } - newlines_above { - #we get a newlines_above instruction when received at column 1 - #In some cases we want to treat that as request to insert a new blank line above, and move our row 1 down (staying with the data) - #in other cases - we want to treat at column 1 the same as any other - - puts "--->newlines_above" - puts "rinfo: $rinfo" - #renderline doesn't advance the row for us - the caller has the choice to implement or not - set row $post_render_row - set col $post_render_col - if {$insert_lines_above > 0} { - set row $renderedrow - set outputlines [linsert $outputlines $renderedrow-1 {*}[lrepeat $insert_lines_above ""]] - incr row [expr {$insert_lines_above -1}] ;#we should end up on the same line of text (at a different index), with new empties inserted above - #? set row $post_render_row #can renderline tell us? - } - } - newlines_below { - #obsolete? - use for ANSI insert lines sequence - if {$data_mode == 0} { - puts --->nl_below - set row $post_render_row - set col $post_render_col - if {$insert_lines_below == 1} { - #set lhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $rendered]] - #set lhs [textblock::frame -title rendered -subtitle "row-$renderedrow" $lhs] - #set rhs "" - #if {$overflow_right ne ""} { - # set rhs [overtype::left -width 40 -wrap 1 "" [ansistring VIEWSTYLE -lf 1 -vt 1 $overflow_right]] - # set rhs [textblock::frame -title overflow_right $rhs] - #} - #puts [textblock::join $lhs $rhs] - - #rendered - append rendered $overflow_right - # - - - set overflow_right "" - set row $renderedrow - #only add newline if we're at the bottom - if {$row > [llength $outputlines]} { - lappend outputlines {*}[lrepeat $insert_lines_below ""] - } - incr row $insert_lines_below - set col $opt_startcolumn - } - } else { - set row $post_render_row - if {$post_render_row > [llength $outputlines]} { - if {$opt_appendlines} { - set diff [expr {$post_render_row - [llength $outputlines]}] - if {$diff > 0} { - lappend outputlines {*}[lrepeat $diff ""] - } - lappend outputlines "" - } - } else { - set existingdata [lindex $outputlines [expr {$post_render_row -1}]] - set lastdatacol [punk::ansi::printing_length $existingdata] - if {$lastdatacol < $renderwidth} { - set col [expr {$lastdatacol+1}] - } else { - set col $renderwidth - } - } - } - } - wrapmoveforward { - #doesn't seem to be used by fruit.ans testfile - #used by dzds.ans - #note that cursor_forward may move deep into the next line - or even span multiple lines !TODO - set c $renderwidth - set r $post_render_row - if {$post_render_col > $renderwidth} { - set i $c - while {$i <= $post_render_col} { - if {$c == $renderwidth+1} { - incr r - if {$opt_appendlines} { - if {$r < [llength $outputlines]} { - lappend outputlines "" - } - } - set c $opt_startcolumn - } else { - incr c - } - incr i - } - set col $c - } else { - #why are we getting this instruction then? - puts stderr "wrapmoveforward - test" - set r [expr {$post_render_row +1}] - set c $post_render_col - } - set row $r - set col $c - } - wrapmovebackward { - set c $renderwidth - set r $post_render_row - if {$post_render_col < 1} { - set c 1 - set i $c - while {$i >= $post_render_col} { - if {$c == 0} { - if {$r > 1} { - incr r -1 - set c $renderwidth - } else { - #leave r at 1 set c 1 - #testfile besthpav.ans first line top left border alignment - set c 1 - break - } - } else { - incr c -1 - } - incr i -1 - } - set col $c - } else { - puts stderr "Wrapmovebackward - but postrendercol >= 1???" - } - set row $r - set col $c - } - overflow { - #normal single-width grapheme overflow - #puts "----normal overflow --- [ansistring VIEWSTYLE -lf 1 -nul 1 -vt 1 $rendered]" - set row $post_render_row ;#renderline will not advance row when reporting overflow char - if {[tcl::dict::get $vtstate autowrap_mode]} { - incr row - set col $opt_startcolumn ;#whether wrap or not - next data is at column 1 ?? - } else { - set col $post_render_col - #set unapplied "" ;#this seems wrong? - #set unapplied [tcl::string::range $unapplied 1 end] - #The overflow can only be triggered by a grapheme (todo cluster?) - but our unapplied could contain SGR codes prior to the grapheme that triggered overflow - so we need to skip beyond any SGRs - #There may be more than one, because although the stack leading up to overflow may have been merged - codes between the last column and the overflowing grapheme will remain separate - #We don't expect any movement or other ANSI codes - as if they came before the grapheme, they would have triggered a different instruction to 'overflow' - set idx 0 - set next_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set next_grapheme_index $idx - break - } - incr idx - } - assert {$next_grapheme_index >= 0} - #drop the overflow grapheme - keeping all codes in place. - set unapplied [join [lreplace $unapplied_list $next_grapheme_index $next_grapheme_index] ""] - #we need to run the reduced unapplied on the same line - further graphemes will just overflow again, but codes or control chars could trigger jumps to other lines - - set overflow_handled 1 - #handled by dropping overflow if any - } - } - overflow_splitchar { - set row $post_render_row ;#renderline will not advance row when reporting overflow char - - #2nd half of grapheme would overflow - treggering grapheme is returned in unapplied. There may also be overflow_right from earlier inserts - #todo - consider various options .. re-render a single trailing space or placeholder on same output line, etc - if {[tcl::dict::get $vtstate autowrap_mode]} { - if {$renderwidth < 2} { - #edge case of rendering to a single column output - any 2w char will just cause a loop if we don't substitute with something, or drop the character - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } else { - set col $opt_startcolumn - incr row - } - } else { - set overflow_handled 1 - #handled by dropping entire overflow if any - if {$renderwidth < 2} { - set idx 0 - set triggering_grapheme_index -1 - foreach u $unapplied_list { - if {![punk::ansi::ta::detect $u]} { - set triggering_grapheme_index $idx - break - } - incr idx - } - set unapplied [join [lreplace $unapplied_list $triggering_grapheme_index $triggering_grapheme_index $opt_exposed1] ""] - } - } - - } - vt { - - #can vt add a line like a linefeed can? - set row $post_render_row - set col $post_render_col - } - set_window_title { - set newtitle [lindex $instruction 1] - puts stderr "overtype::renderspace set_window_title [ansistring VIEW $newtitle] instruction '$instruction'" - # - } - reset_colour_palette { - puts stderr "overtype::renderspace instruction '$instruction' unimplemented" - } - default { - puts stderr "overtype::renderspace unhandled renderline instruction '$instruction'" - } - - } - - - if {!$opt_expand_right && ![tcl::dict::get $vtstate autowrap_mode]} { - #not allowed to overflow column or wrap therefore we get overflow data to truncate - if {[tcl::dict::get $opts -ellipsis]} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[tcl::string::trim $lostdata] eq ""} { - set show_ellipsis 0 - } - #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - if {[tcl::string::trim [punk::ansi::ansistrip $lostdata]] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - set overflow_handled 1 - } else { - #no wrap - no ellipsis - silently truncate - set overflow_handled 1 - } - } - - - - if {$renderedrow <= [llength $outputlines]} { - lset outputlines [expr {$renderedrow-1}] $rendered - } else { - if {$opt_appendlines} { - lappend outputlines $rendered - } else { - #? - lset outputlines [expr {$renderedrow-1}] $rendered - } - } - - if {!$overflow_handled} { - append nextprefix $overflow_right - } - - append nextprefix $unapplied - - if 0 { - if {$nextprefix ne ""} { - set nextoveridx [expr {$overidx+1}] - if {$nextoveridx >= [llength $inputchunks]} { - lappend inputchunks $nextprefix - } else { - #lset overlines $nextoveridx $nextprefix[lindex $overlines $nextoveridx] - set inputchunks [linsert $inputchunks $nextoveridx $nextprefix] - } - } - } - - if {$nextprefix ne ""} { - set inputchunks [linsert $inputchunks 0 $nextprefix] - } - - - incr overidx - incr loop - if {$loop >= $looplimit} { - puts stderr "overtype::renderspace looplimit reached ($looplimit)" - lappend outputlines "[a+ red bold] - looplimit $looplimit reached[a]" - set Y [a+ yellow bold] - set RST [a] - set sep_header ----DEBUG----- - set debugmsg "" - append debugmsg "${Y}${sep_header}${RST}" \n - append debugmsg "looplimit $looplimit reached\n" - append debugmsg "data_mode:$data_mode\n" - append debugmsg "opt_appendlines:$opt_appendlines\n" - append debugmsg "prev_row :[tcl::dict::get $renderopts -cursor_row]\n" - append debugmsg "prev_col :[tcl::dict::get $renderopts -cursor_column]\n" - tcl::dict::for {k v} $rinfo { - append debugmsg "${Y}$k [ansistring VIEW -lf 1 -vt 1 $v]$RST" \n - } - append debugmsg "${Y}[string repeat - [string length $sep_header]]$RST" \n - - puts stdout $debugmsg - #todo - config regarding error dumps rather than just dumping in working dir - set fd [open [pwd]/error_overtype.txt w] - puts $fd $debugmsg - close $fd - error $debugmsg - break - } - } - - set result [join $outputlines \n] - if {!$opt_info} { - return $result - } else { - #emit to debug window like basictelnet does? make debug configurable as syslog or even a telnet server to allow on 2nd window? - #append result \n$instruction_stats\n - set inforesult [dict create\ - result $result\ - last_instruction $instruction\ - instruction_stats $instruction_stats\ - ] - if {$opt_info == 2} { - return [pdict -channel none inforesult] - } else { - return $inforesult - } - } - } - - #todo - left-right ellipsis ? - proc centre {args} { - variable default_ellipsis_horizontal - if {[llength $args] < 2} { - error {usage: ?-transparent [0|1]? ?-bias [left|right]? ?-overflow [1|0]? undertext overtext} - } - - foreach {underblock overblock} [lrange $args end-1 end] break - - #todo - vertical vs horizontal overflow for blocks - set opts [tcl::dict::create\ - -bias left\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - ] - set argsflags [lrange $args 0 end-2] - foreach {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 { - tcl::dict::set opts $k $v - } - default { - set known_opts [tcl::dict::keys $opts] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - #set opts [tcl::dict::merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [tcl::dict::get $opts -transparent] - set opt_ellipsis [tcl::dict::get $opts -ellipsis] - set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] - set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] - set opt_exposed1 [tcl::dict::get $opts -exposed1] - set opt_exposed2 [tcl::dict::get $opts -exposed2] - # -- --- --- --- --- --- - - - set underblock [tcl::string::map {\r\n \n} $underblock] - set overblock [tcl::string::map {\r\n \n} $overblock] - - set underlines [split $underblock \n] - #set renderwidth [tcl::mathfunc::max {*}[lmap v $underlines {punk::ansi::printing_length $v}]] - lassign [blocksize $underblock] _w renderwidth _h renderheight - set overlines [split $overblock \n] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {$renderwidth - $overblock_width}] - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[tcl::string::tolower [tcl::dict::get $opts -bias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $renderwidth} { - set udiff [expr {$renderwidth - $ulen}] - set undertext "$undertext[string repeat { } $udiff]" - } - set undertext $replay_codes_underlay$undertext - set overtext $replay_codes_overlay$overtext - - set overflowlength [expr {$overtext_datalen - $renderwidth}] - #review - right-to-left langs should elide on left! - extra option required - - if {$overflowlength > 0} { - #overlay line wider or equal - #review - we still expand_right for centred for now.. possibly should do something like -expand_leftright with ellipsis each end? - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right [tcl::dict::get $opts -overflow] -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rendered [tcl::dict::get $rinfo result] - set overflow_right [tcl::dict::get $rinfo overflow_right] - set unapplied [tcl::dict::get $rinfo unapplied] - #todo - get replay_codes from overflow_right instead of wherever it was truncated? - - #overlay line data is wider - trim if overflow not specified in opts - and overtype an ellipsis at right if it was specified - if {![tcl::dict::get $opts -overflow]} { - #lappend outputlines [tcl::string::range $overtext 0 [expr {$renderwidth - 1}]] - #set overtext [tcl::string::range $overtext 0 $renderwidth-1 ] - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #don't use tcl::string::range on ANSI data - #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[tcl::string::trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::right $rendered $opt_ellipsistext] - } - } - } - lappend outputlines $rendered - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent $undertext $overtext] - } else { - #background block is wider than or equal to data for this line - #lappend outputlines [renderline -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - set rinfo [renderline -info 1 -insert_mode 0 -startcolumn [expr {$left_exposed + 1}] -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 $undertext $overtext] - lappend outputlines [tcl::dict::get $rinfo result] - } - set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] - set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] - } - return [join $outputlines \n] - } - - #overtype::right is for a rendered ragged underblock and a rendered ragged overblock - #ie we can determine the block width for bost by examining the lines and picking the longest. - # - proc right {args} { - #NOT the same as align-right - which should be done to the overblock first if required - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - foreach {underblock overblock} [lrange $args end-1 end] break - - set opts [tcl::dict::create\ - -bias ignored\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -align "left"\ - ] - set argsflags [lrange $args 0 end-2] - tcl::dict::for {k v} $argsflags { - switch -- $k { - -bias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -align { - tcl::dict::set opts $k $v - } - default { - set known_opts [tcl::dict::keys $opts] - error "overtype::centre unknown option '$k'. Known options: $known_opts" - } - } - } - #set opts [tcl::dict::merge $defaults $argsflags] - # -- --- --- --- --- --- - set opt_transparent [tcl::dict::get $opts -transparent] - set opt_ellipsis [tcl::dict::get $opts -ellipsis] - set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] - set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] - set opt_overflow [tcl::dict::get $opts -overflow] - set opt_exposed1 [tcl::dict::get $opts -exposed1] - set opt_exposed2 [tcl::dict::get $opts -exposed2] - set opt_align [tcl::dict::get $opts -align] - # -- --- --- --- --- --- - - set underblock [tcl::string::map {\r\n \n} $underblock] - set overblock [tcl::string::map {\r\n \n} $overblock] - - set underlines [split $underblock \n] - lassign [blocksize $underblock] _w renderwidth _h renderheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$renderwidth - $overblock_width)}] - set left_exposed $under_exposed_max - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $renderwidth} { - set udiff [expr {$renderwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_align { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext $replay_codes_underlay$undertext - set overtext $replay_codes_overlay$overtext - - set overflowlength [expr {$overtext_datalen - $renderwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline\ - -info 1\ - -insert_mode 0\ - -transparent $opt_transparent\ - -exposed1 $opt_exposed1 -exposed2 $opt_exposed2\ - -overflow $opt_overflow\ - -startcolumn [expr {1 + $startoffset}]\ - $undertext $overtext] - set replay_codes [tcl::dict::get $rinfo replay_codes] - set rendered [tcl::dict::get $rinfo result] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - if {[tcl::string::trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set ellipsis $replay_codes$opt_ellipsistext - #todo - overflow on left if allign = right?? - set rendered [overtype::right $rendered $ellipsis] - } - } - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need overflow(exapnd_right) here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - lappend outputlines [tcl::dict::get $rinfo result] - } - set replay_codes [tcl::dict::get $rinfo replay_codes] - set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] - set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] - } - - proc left {args} { - overtype::block -blockalign left {*}$args - } - #overtype a (possibly ragged) underblock with a (possibly ragged) overblock - proc block {args} { - variable default_ellipsis_horizontal - # @d !todo - implement overflow, length checks etc - - if {[llength $args] < 2} { - error {usage: ?-blockalign left|centre|right? ?-textalign left|centre|right? ?-overflow [1|0]? ?-transparent 0|? undertext overtext} - } - #foreach {underblock overblock} [lrange $args end-1 end] break - lassign [lrange $args end-1 end] underblock overblock - - set opts [tcl::dict::create\ - -ellipsis 0\ - -ellipsistext $default_ellipsis_horizontal\ - -ellipsiswhitespace 0\ - -overflow 0\ - -transparent 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -textalign "left"\ - -textvertical "top"\ - -blockalign "left"\ - -blockalignbias left\ - -blockvertical "top"\ - ] - set argsflags [lrange $args 0 end-2] - tcl::dict::for {k v} $argsflags { - switch -- $k { - -blockalignbias - -ellipsis - -ellipsistext - -ellipsiswhitespace - -overflow - -transparent - -exposed1 - -exposed2 - -textalign - -blockalign - -blockvertical { - tcl::dict::set opts $k $v - } - default { - error "overtype::block unknown option '$k'. Known options: [tcl::dict::keys $opts]" - } - } - } - # -- --- --- --- --- --- - set opt_transparent [tcl::dict::get $opts -transparent] - set opt_ellipsis [tcl::dict::get $opts -ellipsis] - set opt_ellipsistext [tcl::dict::get $opts -ellipsistext] - set opt_ellipsiswhitespace [tcl::dict::get $opts -ellipsiswhitespace] - set opt_overflow [tcl::dict::get $opts -overflow] - set opt_exposed1 [tcl::dict::get $opts -exposed1] - set opt_exposed2 [tcl::dict::get $opts -exposed2] - set opt_textalign [tcl::dict::get $opts -textalign] - set opt_blockalign [tcl::dict::get $opts -blockalign] - if {$opt_blockalign eq "center"} { - set opt_blockalign "centre" - } - # -- --- --- --- --- --- - - set underblock [tcl::string::map {\r\n \n} $underblock] - set overblock [tcl::string::map {\r\n \n} $overblock] - - set underlines [split $underblock \n] - lassign [blocksize $underblock] _w renderwidth _h renderheight - set overlines [split $overblock \n] - #set overblock_width [tcl::mathfunc::max {*}[lmap v $overlines {punk::ansi::printing_length $v}]] - lassign [blocksize $overblock] _w overblock_width _h overblock_height - set under_exposed_max [expr {max(0,$renderwidth - $overblock_width)}] - - switch -- $opt_blockalign { - left { - set left_exposed 0 - } - right { - set left_exposed $under_exposed_max - } - centre { - if {$under_exposed_max > 0} { - #background block is wider - if {$under_exposed_max % 2 == 0} { - #even left/right exposure - set left_exposed [expr {$under_exposed_max / 2}] - } else { - set beforehalf [expr {$under_exposed_max / 2}] ;#1 less than half due to integer division - if {[tcl::string::tolower [tcl::dict::get $opts -blockalignbias]] eq "left"} { - set left_exposed $beforehalf - } else { - #bias to the right - set left_exposed [expr {$beforehalf + 1}] - } - } - } else { - set left_exposed 0 - } - } - default { - set left_exposed 0 - } - } - - - - set outputlines [list] - if {[punk::ansi::ta::detect_sgr [lindex $overlines 0]]} { - set replay_codes "[punk::ansi::a]" - } else { - set replay_codes "" - } - set replay_codes_underlay "" - set replay_codes_overlay "" - foreach undertext $underlines overtext $overlines { - set overtext_datalen [punk::ansi::printing_length $overtext] - set ulen [punk::ansi::printing_length $undertext] - if {$ulen < $renderwidth} { - set udiff [expr {$renderwidth - $ulen}] - #puts xxx - append undertext [string repeat { } $udiff] - } - if {$overtext_datalen < $overblock_width} { - set odiff [expr {$overblock_width - $overtext_datalen}] - switch -- $opt_textalign { - left { - set startoffset 0 - } - right { - set startoffset $odiff - } - default { - set half [expr {$odiff / 2}] - #set lhs [string repeat { } $half] - #set righthalf [expr {$odiff - $half}] ;#remainder - may be one more - so we are biased left - #set rhs [string repeat { } $righthalf] - set startoffset $half - } - } - } else { - set startoffset 0 ;#negative? - } - - set undertext $replay_codes_underlay$undertext - set overtext $replay_codes_overlay$overtext - - set overflowlength [expr {$overtext_datalen - $renderwidth}] - if {$overflowlength > 0} { - #raw overtext wider than undertext column - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -exposed1 $opt_exposed1 -exposed2 $opt_exposed2 -expand_right $opt_overflow -startcolumn [expr {1 + $startoffset}] $undertext $overtext] - set replay_codes [tcl::dict::get $rinfo replay_codes] - set rendered [tcl::dict::get $rinfo result] - set overflow_right [tcl::dict::get $rinfo overflow_right] - set unapplied [tcl::dict::get $rinfo unapplied] - if {!$opt_overflow} { - if {$opt_ellipsis} { - set show_ellipsis 1 - if {!$opt_ellipsiswhitespace} { - #we don't want ellipsis if only whitespace was lost - #don't use tcl::string::range on ANSI data - #set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - set lostdata "" - if {$overflow_right ne ""} { - append lostdata $overflow_right - } - if {$unapplied ne ""} { - append lostdata $unapplied - } - if {[tcl::string::trim $lostdata] eq ""} { - set show_ellipsis 0 - } - } - if {$show_ellipsis} { - set rendered [overtype::block -blockalign right $rendered $opt_ellipsistext] - } - } - - #if {$opt_ellipsis} { - # set show_ellipsis 1 - # if {!$opt_ellipsiswhitespace} { - # #we don't want ellipsis if only whitespace was lost - # set lostdata [tcl::string::range $overtext end-[expr {$overflowlength-1}] end] - # if {[tcl::string::trim $lostdata] eq ""} { - # set show_ellipsis 0 - # } - # } - # if {$show_ellipsis} { - # set ellipsis [tcl::string::cat $replay_codes $opt_ellipsistext] - # #todo - overflow on left if allign = right?? - # set rendered [overtype::right $rendered $ellipsis] - # } - #} - } - lappend outputlines $rendered - } else { - #padded overtext - #lappend outputlines [renderline -insert_mode 0 -transparent $opt_transparent -startcolumn [expr {$left_exposed + 1}] $undertext $overtext] - #Note - we still need expand_right here - as although the overtext is short - it may oveflow due to the startoffset - set rinfo [renderline -info 1 -insert_mode 0 -transparent $opt_transparent -expand_right $opt_overflow -startcolumn [expr {$left_exposed + 1 + $startoffset}] $undertext $overtext] - #puts stderr "--> [ansistring VIEW -lf 1 -nul 1 $rinfo] <--" - set overflow_right [tcl::dict::get $rinfo overflow_right] - set unapplied [tcl::dict::get $rinfo unapplied] - lappend outputlines [tcl::dict::get $rinfo result] - } - set replay_codes [tcl::dict::get $rinfo replay_codes] - set replay_codes_underlay [tcl::dict::get $rinfo replay_codes_underlay] - set replay_codes_overlay [tcl::dict::get $rinfo replay_codes_overlay] - } - - return [join $outputlines \n] - } - - variable optimise_ptruns 10 ;# can be set to zero to disable the ptruns branches - - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # renderline written from a left-right line orientation perspective as a first-shot at getting something useful. - # ultimately right-to-left, top-to-bottom and bottom-to-top are probably needed. - # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### - # - # - #-returnextra enables returning of overflow and length - #review - use punk::ansi::ta::detect to short-circuit processing and do simpler string calcs as an optimisation? - #review - DECSWL/DECDWL double width line codes - very difficult/impossible to align and compose with other elements - #(could render it by faking it with sixels and a lot of work - find/make a sixel font and ensure it's exactly 2 cols per char? - # This would probably be impractical to support for different fonts) - #todo - review transparency issues with single/double width characters - #bidi - need a base direction and concept of directional runs for RTL vs LTR - may be best handled at another layer? - proc renderline {args} { - #*** !doctools - #[call [fun overtype::renderline] [arg args] ] - #[para] renderline is the core engine for overtype string processing (frames & textblocks), and the raw mode commandline repl for the Tcl Punk Shell - #[para] It is also a central part of an ansi (micro) virtual terminal-emulator of sorts - #[para] This system does a half decent job at rendering 90's ANSI art to manipulable colour text blocks that can be joined & framed for layout display within a unix or windows terminal - #[para] Renderline helps maintain ANSI text styling reset/replay codes so that the styling of one block doesn't affect another. - #[para] Calling on the punk::ansi library - it can coalesce codes to keep the size down. - #[para] It is a giant mess of doing exactly what common wisdom says not to do... lots at once. - #[para] renderline is part of the Unicode and ANSI aware Overtype system which 'renders' a block of text onto a static underlay - #[para] The underlay is generally expected to be an ordered set of lines or a rectangular text block analogous to a terminal screen - but it can also be ragged in line length, or just blank. - #[para] The overlay couuld be similar - in which case it may often be used to overwrite a column or section of the underlay. - #[para] The overlay could however be a sequence of ANSI-laden text that jumps all over the place. - # - #[para] renderline itself only deals with a single line - or sometimes a single character. It is generally called from a loop that does further terminal-like or textblock processing. - #[para] By suppyling the -info 1 option - it can return various fields indicating the state of the render. - #[para] The main 3 are the result, overflow_right, and unapplied. - #[para] Renderline handles cursor movements from either keystrokes or ANSI sequences but for a full system the aforementioned loop will need to be in place to manage the set of lines under manipulation. - - #puts stderr "renderline '$args'" - variable optimise_ptruns - - if {[llength $args] < 2} { - error {usage: ?-info 0|1? ?-startcolumn ? ?-cursor_column ? ?-cursor_row |""? ?-transparent [0|1|]? ?-expand_right [1|0]? undertext overtext} - } - set under [lindex $args end-1] - set over [lindex $args end] - #lassign [lrange $args end-1 end] under over - if {[string last \n $under] >= 0} { - error "overtype::renderline not allowed to contain newlines in undertext" - } - #if {[string first \n $over] >=0 || [string first \n $under] >= 0} { - # error "overtype::renderline not allowed to contain newlines" - #} - - #generally faster to create a new dict in the proc than to use a namespace variable to create dict once and link to variable (2024 8.6/8.7) - set opts [tcl::dict::create\ - -etabs 0\ - -width \uFFEF\ - -expand_right 0\ - -transparent 0\ - -startcolumn 1\ - -cursor_column 1\ - -cursor_row ""\ - -insert_mode 1\ - -crm_mode 0\ - -autowrap_mode 1\ - -reverse_mode 0\ - -info 0\ - -exposed1 \uFFFD\ - -exposed2 \uFFFD\ - -cursor_restore_attributes ""\ - -cp437 0\ - -experimental {}\ - ] - #-cursor_restore_attributes only - for replay stack - position and actual setting/restoring handled by throwback to caller - - #cursor_row, when numeric will allow detection of certain row moves that are still within our row - allowing us to avoid an early return - #An empty string for cursor_row tells us we have no info about our own row context, and to return with an unapplied string if any row move occurs - - #exposed1 and exposed2 for first and second col of underying 2wide char which is truncated by transparency, currsor movements to 2nd charcol, or overflow/expand_right - #todo - return info about such grapheme 'cuts' in -info structure and/or create option to raise an error - - set argsflags [lrange $args 0 end-2] - tcl::dict::for {k v} $argsflags { - switch -- $k { - -experimental - -cp437 - -width - -expand_right - -transparent - -startcolumn - -cursor_column - -cursor_row - - -crm_mode - -insert_mode - -autowrap_mode - -reverse_mode - - -info - -exposed1 - -exposed2 - -cursor_restore_attributes { - tcl::dict::set opts $k $v - } - default { - error "overtype::renderline unknown option '$k'. Known options: [tcl::dict::keys $opts]" - } - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_width [tcl::dict::get $opts -width] - set opt_etabs [tcl::dict::get $opts -etabs] - set opt_expand_right [tcl::dict::get $opts -expand_right] - set opt_colstart [tcl::dict::get $opts -startcolumn] ;#lhs limit for overlay - an offset to cursor_column - first visible column is 1. 0 or < 0 are before the start of the underlay - set opt_colcursor [tcl::dict::get $opts -cursor_column];#start cursor column relative to overlay - set opt_row_context [tcl::dict::get $opts -cursor_row] - if {[string length $opt_row_context]} { - if {![tcl::string::is integer -strict $opt_row_context] || $opt_row_context <1 } { - error "overtype::renderline -cursor_row must be empty for unspecified/unknown or a non-zero positive integer. received: '$opt_row_context'" - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - #The _mode flags correspond to terminal modes that can be set/reset via escape sequences (e.g DECAWM wraparound mode) - set opt_insert_mode [tcl::dict::get $opts -insert_mode];#should usually be 1 for each new line in editor mode but must be initialised to 1 externally (review) - #default is for overtype - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_autowrap_mode [tcl::dict::get $opts -autowrap_mode] ;#DECAWM - char or movement can go beyond leftmost/rightmost col to prev/next line - set opt_reverse_mode [tcl::dict::get $opts -reverse_mode] ;#DECSNM - set opt_crm_mode [tcl::dict::get $opts -crm_mode];# CRM - show control character mode - # -- --- --- --- --- --- --- --- --- --- --- --- - set temp_cursor_saved [tcl::dict::get $opts -cursor_restore_attributes] - - set cp437_glyphs [tcl::dict::get $opts -cp437] - set cp437_map [tcl::dict::create] - if {$cp437_glyphs} { - set cp437_map [set ::punk::ansi::cp437_map] - #for cp437 images we need to map these *after* splitting ansi - #some old files might use newline for its glyph.. but we can't easily support that. - #Not sure how old files did it.. maybe cr lf in sequence was newline and any lone cr or lf were displayed as glyphs? - tcl::dict::unset cp437_map \n - } - - set opt_transparent [tcl::dict::get $opts -transparent] - if {$opt_transparent eq "0"} { - set do_transparency 0 - } else { - set do_transparency 1 - if {$opt_transparent eq "1"} { - set opt_transparent {[\s]} - } - } - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_returnextra [tcl::dict::get $opts -info] - # -- --- --- --- --- --- --- --- --- --- --- --- - set opt_exposed1 [tcl::dict::get $opts -exposed1] - set opt_exposed2 [tcl::dict::get $opts -exposed2] - # -- --- --- --- --- --- --- --- --- --- --- --- - - if {$opt_row_context eq ""} { - set cursor_row 1 - } else { - set cursor_row $opt_row_context - } - - set insert_mode $opt_insert_mode ;#default 1 - set autowrap_mode $opt_autowrap_mode ;#default 1 - set crm_mode $opt_crm_mode ;#default 0 (Show Control Character mode) - set reverse_mode $opt_reverse_mode - - #----- - # - if {[info exists punk::console::tabwidth]} { - #punk console is updated if punk::console::set_tabstop_width is used or rep is started/restarted - #It is way too slow to test the current width by querying the terminal here - so it could conceivably get out of sync - #todo - we also need to handle the new threaded repl where console config is in a different thread. - # - also - concept of sub-regions being mini-consoles with their own settings - 'id' for console, or use in/out channels as id? - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - - set overdata $over - if {!$cp437_glyphs} { - #REVIEW! tabify will give different answers for an ANSI colourised string vs plain text - if {!$opt_etabs} { - if {[string first \t $under] >= 0} { - #set under [textutil::tabify::untabify2 $under] - set under [textutil::tabify::untabifyLine $under $tw] - } - if {[string first \t $over] >= 0} { - #set overdata [textutil::tabify::untabify2 $over] - set overdata [textutil::tabify::untabifyLine $over $tw] - } - } - } - #------- - - #ta_detect ansi and do simpler processing? - - #we repeat tests for grapheme width in different loops - rather than create another datastructure to store widths based on column, - #we'll use the grapheme_width_cached function as a lookup table of all graphemes encountered - as there will often be repeats in different positions anyway. - - # -- --- --- --- --- --- --- --- - if {$under ne ""} { - if {[punk::ansi::ta::detect $under]} { - set undermap [punk::ansi::ta::split_codes_single $under] - } else { - #single plaintext part - set undermap [list $under] - } - } else { - set undermap [list] - } - set understacks [list] - set understacks_gx [list] - set pm_list [list] - - set i_u -1 ;#underlay may legitimately be empty - set undercols [list] - set u_codestack [list] - #u_gx_stack probably isn't really a stack - I don't know if g0 g1 can stack or not - for now we support only g0 anyway - set u_gx_stack [list] ;#separate stack for g0 (g1 g2 g3?) graphics on and off (DEC special graphics) - #set pt_underchars "" ;#for string_columns length calculation for expand_right 0 truncation - set remainder [list] ;#for returnextra - foreach {pt code} $undermap { - #pt = plain text - #append pt_underchars $pt - if {$pt ne ""} { - if {$cp437_glyphs} { - set pt [tcl::string::map $cp437_map $pt] - } - set is_ptrun 0 - if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} { - set p1 [tcl::string::index $pt 0] - set hex [format %x [scan $p1 %c]] ;#punk::char::char_hex - set re [tcl::string::cat {^[} \\U$hex {]+$}] - set is_ptrun [regexp $re $pt] - } - if {$is_ptrun} { - #switch -- $p1 { - # " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - # a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - # z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - # set width 1 - # } - # default { - # if {$p1 eq "\u0000"} { - # #use null as empty cell representation - review - # #use of this will probably collide with some application at some point - # #consider an option to set the empty cell character - # set width 1 - # } else { - # set width [grapheme_width_cached $p1] ;# when zero??? - # } - # } - #} - set width [grapheme_width_cached $p1] ;# when zero??? - set ptlen [string length $pt] - if {$width <= 1} { - #review - 0 and 1? - incr i_u $ptlen - lappend understacks {*}[lrepeat $ptlen $u_codestack] - lappend understacks_gx {*}[lrepeat $ptlen $u_gx_stack] - lappend undercols {*}[lrepeat $ptlen $p1] - } else { - incr i_u $ptlen ;#2nd col empty str - so same as above - set 2ptlen [expr {$ptlen * 2}] - lappend understacks {*}[lrepeat $2ptlen $u_codestack] - lappend understacks_gx {*}[lrepeat $2ptlen $u_gx_stack] - set l [concat {*}[lrepeat $ptlen [list $p1 ""]] - lappend undercols {*}$l - unset l - } - - } else { - foreach grapheme [punk::char::grapheme_split $pt] { - #an ugly but easy hack to serve *some* common case ascii quickly with byte-compiled literal switch - feels dirty. - #.. but even 0.5uS per char (grapheme_width_cached) adds up quickly when stitching lots of lines together. - #todo - test decimal value instead, compare performance - switch -- $grapheme { - " " - - - _ - ! - @ - # - $ - % - ^ - & - * - = - + - : - . - , - / - | - ? - - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - - z - A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 { - set width 1 - } - default { - if {$grapheme eq "\u0000"} { - #use null as empty cell representation - review - #use of this will probably collide with some application at some point - #consider an option to set the empty cell character - set width 1 - } else { - #zero width still acts as 1 below??? review what should happen - set width [grapheme_width_cached $grapheme] - #we still want most controls and other zero-length codepoints such as \u200d (zero width joiner) to stay zero-length - #we substitute lone ESC that weren't captured within ANSI context as a debugging aid to see malformed ANSI - #todo - default to off and add a flag (?) to enable this substitution - set sub_stray_escapes 0 - if {$sub_stray_escapes && $width == 0} { - if {$grapheme eq "\x1b"} { - set gvis [ansistring VIEW $grapheme] ;#can only use with graphemes that have a single replacement char.. - set grapheme $gvis - set width 1 - } - } - } - } - } - - #set width [grapheme_width_cached $grapheme] - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - - lappend undercols $grapheme - if {$width > 1} { - #presumably there are no triple-column or wider unicode chars.. until the aliens arrive.(?) - #but what about emoji combinations etc - can they be wider than 2? - #todo - if -etabs enabled - then we treat \t as the width determined by our elastic tabstop - incr i_u - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - lappend undercols "" - } - } - - } - } - #underlay should already have been rendered and not have non-sgr codes - but let's retain the check for them and not stack them if other codes are here - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #keep any remaining PMs in place - if {$code ne ""} { - set c1c2 [tcl::string::range $code 0 1] - - set leadernorm [tcl::string::range [tcl::string::map [list\ - \x1b\[ 7CSI\ - \x9b 8CSI\ - \x1b\( 7GFX\ - \x1b^ 7PMX\ - \x1bX 7SOS\ - ] $c1c2] 0 3];# leadernorm is 1st 2 chars mapped to normalised indicator - or is original 2 chars - - switch -- $leadernorm { - 7CSI - 8CSI { - #need to exclude certain leaders after the lb e.g < for SGR 1006 mouse - #REVIEW - what else could end in m but be mistaken as a normal SGR code here? - set maybemouse "" - if {[tcl::string::index $c1c2 0] eq "\x1b"} { - set maybemouse [tcl::string::index $code 2] - } - - if {$maybemouse ne "<" && [tcl::string::index $code end] eq "m"} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set u_codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set u_codestack [list $code] - } else { - #basic simplification first.. straight dups - set dup_posns [lsearch -all -exact $u_codestack $code] ;#-exact because of square-bracket glob chars - set u_codestack [lremove $u_codestack {*}$dup_posns] - lappend u_codestack $code - } - } - } - 7GFX { - switch -- [tcl::string::index $code 2] { - "0" { - set u_gx_stack [list gx0_on] ;#we'd better use a placeholder - or debugging will probably get into a big mess - } - B { - set u_gx_stack [list] - } - } - } - 7PMX - 7SOS { - #we can have PMs or SOS (start of string) in the underlay - though mostly the PMs should have been processed.. - #attach the PM/SOS (entire ANSI sequence) to the previous grapheme! - #It should not affect the size - but terminal display may get thrown out if terminal doesn't support them. - - #note that there may in theory already be ANSI stored - we don't assume it was a pure grapheme string - set graphemeplus [lindex $undercols end] - if {$graphemeplus ne "\0"} { - append graphemeplus $code - } else { - set graphemeplus $code - } - lset undercols end $graphemeplus - #The grapheme_width_cached function will be called on this later - and doesn't account for ansi. - #we need to manually cache the item with it's proper width - variable grapheme_widths - #stripped and plus version keys pointing to same length - dict set grapheme_widths $graphemeplus [grapheme_width_cached [::punk::ansi::ansistrip $graphemeplus]] - - } - default { - - } - - } - - #if {[punk::ansi::codetype::is_sgr_reset $code]} { - # #set u_codestack [list] - #} elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - #} elseif {[punk::ansi::codetype::is_sgr $code]} { - #} else { - # #leave SGR stack as is - # if {[punk::ansi::codetype::is_gx_open $code]} { - # } elseif {[punk::ansi::codetype::is_gx_close $code]} { - # } - #} - } - #consider also if there are other codes that should be stacked..? - } - - #NULL empty cell indicator - if {$opt_width ne "\uFFEF"} { - if {[llength $understacks]} { - set cs $u_codestack - set gs $u_gx_stack - } else { - set cs [list] - set gs [list] - } - if {[llength $undercols]< $opt_width} { - set diff [expr {$opt_width- [llength $undercols]}] - if {$diff > 0} { - #set undercols [list {*}$undercols {*}[lrepeat $diff "\u0000"]] ;#2024 - much slower - lappend undercols {*}[lrepeat $diff "\u0000"] - lappend understacks {*}[lrepeat $diff $cs] - lappend understacks_gx {*}[lrepeat $diff $gs] - } - } - } - - if {$opt_width ne "\uFFEF"} { - set renderwidth $opt_width - } else { - set renderwidth [llength $undercols] - } - - - if 0 { - # ----------------- - # if we aren't extending understacks & understacks_gx each time we incr idx above the undercols length.. this doesn't really serve a purpose - # Review. - # ----------------- - #replay code for last overlay position in input line - # whether or not we get that far - we need to return it for possible replay on next line - if {[llength $understacks]} { - lappend understacks $u_codestack - lappend understacks_gx $u_gx_stack - } else { - #in case overlay onto emptystring as underlay - lappend understacks [list] - lappend understacks_gx [list] - } - # ----------------- - } - - #trailing codes in effect for underlay - if {[llength $u_codestack]} { - #set replay_codes_underlay [join $u_codestack ""] - set replay_codes_underlay [punk::ansi::codetype::sgr_merge_list {*}$u_codestack] - } else { - set replay_codes_underlay "" - } - - - # -- --- --- --- --- --- --- --- - #### - #if opt_colstart - we need to build a space (or any singlewidth char ?) padding on the left containing the right number of columns. - #this will be processed as transparent - and handle doublewidth underlay characters appropriately - set startpadding [string repeat " " [expr {$opt_colstart -1}]] - #overdata with left padding spaces based on col-start under will show through for left-padding portion regardless of -transparency - if {$startpadding ne "" || $overdata ne ""} { - if {[punk::ansi::ta::detect $overdata]} { - set overmap [punk::ansi::ta::split_codes_single $startpadding$overdata] - } else { - #single plaintext part - set overmap [list $startpadding$overdata] - } - } else { - set overmap [list] - } - #### - - - #todo - detect plain ascii no-ansi input line (aside from leading ansi reset) - #will that allow some optimisations? - - #todo - detect repeated transparent char in overlay - #regexp {^(.)\1+$} ;#backtracking regexp - relatively slow. - # - try set c [string index $data 0]; regexp [string map [list %c% $c] {^[%c%]+$}] $data - #we should be able to optimize to pass through the underlay?? - - #??? - set colcursor $opt_colstart - #TODO - make a little virtual column object - #we need to refer to column1 or columnmin? or columnmax without calculating offsets due to to startcolumn - #need to lock-down what start column means from perspective of ANSI codes moving around - the offset perspective is unclear and a mess. - - - #set re_diacritics {[\u0300-\u036f]+|[\u1ab0-\u1aff]+|[\u1dc0-\u1dff]+|[\u20d0-\u20ff]+|[\ufe20-\ufe2f]+} - #as at 2024-02 punk::char::grapheme_split uses these - not aware of more complex graphemes - - set overstacks [list] - set overstacks_gx [list] - - set o_codestack [list]; #SGR codestack (not other codes such as movement,insert key etc) - set o_gxstack [list] - set pt_overchars "" - set i_o 0 - set overlay_grapheme_control_list [list] ;#tag each with g, sgr or other. 'other' are things like cursor-movement or insert-mode or codes we don't recognise/use - #experiment - set overlay_grapheme_control_stacks [list] - foreach {pt code} $overmap { - if {$pt ne ""} { - #todo - wrap in test for empty pt (we used split_codes_single - and it may be common for sgr sequences to be unmerged and so have empty pts between) - if {$cp437_glyphs} { - set pt [tcl::string::map $cp437_map $pt] - } - append pt_overchars $pt - #will get empty pt between adjacent codes - if {!$crm_mode} { - - set is_ptrun 0 - if {$optimise_ptruns && [tcl::string::length $pt] >= $optimise_ptruns} { - set p1 [tcl::string::index $pt 0] - set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+$}] - set is_ptrun [regexp $re $pt] - - #leading only? we would have to check for graphemes at the trailing boundary? - #set re [tcl::string::cat {^[} \\U[format %x [scan $p1 %c]] {]+}] - #set is_ptrun [regexp -indices $re $pt runrange] - #if {$is_ptrun && 1} { - #} - } - if {$is_ptrun} { - #review - in theory a run over a certain length won't be part of some grapheme combo (graphemes can be long e.g 44?, but not as runs(?)) - #could be edge cases for runs at line end? (should be ok as we include trailing \n in our data) - set len [string length $pt] - set g_element [list g $p1] - - #lappend overstacks {*}[lrepeat $len $o_codestack] - #lappend overstacks_gx {*}[lrepeat $len $o_gxstack] - #incr i_o $len - #lappend overlay_grapheme_control_list {*}[lrepeat $len [list g $p1]] - #lappend overlay_grapheme_control_stacks {*}[lrepeat $len $o_codestack] - - set pi 0 - incr i_o $len - while {$pi < $len} { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - lappend overlay_grapheme_control_list $g_element - lappend overlay_grapheme_control_stacks $o_codestack - incr pi - } - } else { - foreach grapheme [punk::char::grapheme_split $pt] { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - } - } else { - set tsbegin [clock micros] - foreach grapheme_original [punk::char::grapheme_split $pt] { - set pt_crm [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $grapheme_original] - #puts stderr "ptlen [string length $pt] graphemelen[string length $grapheme_original] pt_crmlen[string length $pt_crm] $pt_crm" - foreach grapheme [punk::char::grapheme_split $pt_crm] { - if {$grapheme eq "\n"} { - lappend overlay_grapheme_control_stacks $o_codestack - lappend overlay_grapheme_control_list [list crmcontrol "\x1b\[00001E"] - } else { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - } - } - set elapsed [expr {[clock micros] - $tsbegin}] - puts stderr "ptlen [string length $pt] elapsedus:$elapsed" - } - } - - #only stack SGR (graphics rendition) codes - not title sets, cursor moves etc - #order of if-else based on assumptions: - # that pure resets are fairly common - more so than leading resets with other info - # that non-sgr codes are not that common, so ok to check for resets before verifying it is actually SGR at all. - if {$code ne ""} { - #we need to immediately set crm_mode here if \x1b\[3h received - if {$code eq "\x1b\[3h"} { - set crm_mode 1 - } elseif {$code eq "\x1b\[3l"} { - set crm_mode 0 - } - #else crm_mode could be set either way from options - if {$crm_mode && $code ne "\x1b\[00001E"} { - #treat the code as type 'g' like above - only allow through codes to reset mode REVIEW for now just \x1b\[3l ? - #we need to somehow convert further \n in the graphical rep to an instruction for newline that will bypass further crm_mode processing or we would loop. - set code_as_pt [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $code] - #split using standard split for first foreach loop - grapheme based split when processing 2nd foreach loop - set chars [split $code_as_pt ""] - set codeparts [list] ;#list of 2-el lists each element {crmcontrol } or {g } - foreach c $chars { - if {$c eq "\n"} { - #use CNL (cursor next line) \x1b\[00001E ;#leading zeroes ok for this processor - used as debugging aid to distinguish - lappend codeparts [list crmcontrol "\x1b\[00001E"] - } else { - if {[llength $codeparts] > 0 && [lindex $codeparts end 0] eq "g"} { - set existing [lindex $codeparts end 1] - lset codeparts end [list g [string cat $existing $c]] - } else { - lappend codeparts [list g $c] - } - } - } - - set partidx 0 - foreach record $codeparts { - lassign $record rtype rval - switch -exact -- $rtype { - g { - append pt_overchars $rval - foreach grapheme [punk::char::grapheme_split $rval] { - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - incr i_o - lappend overlay_grapheme_control_list [list g $grapheme] - lappend overlay_grapheme_control_stacks $o_codestack - } - } - crmcontrol { - #leave o_codestack - lappend overlay_grapheme_control_stacks $o_codestack - lappend overlay_grapheme_control_list [list crmcontrol $rval] - } - } - } - } else { - lappend overlay_grapheme_control_stacks $o_codestack - #there will always be an empty code at end due to foreach on 2 vars with odd-sized list ending with pt (overmap coming from perlish split) - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set o_codestack [list "\x1b\[m"] ;#reset better than empty list - fixes some ansi art issues - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set o_codestack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $o_codestack $code] ;#must be -exact because of square-bracket glob chars - set o_codestack [lremove $o_codestack {*}$dup_posns] - lappend o_codestack $code - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[regexp {\x1b7|\x1b\[s} $code]} { - #experiment - #cursor_save - for the replays review. - #jmn - #set temp_cursor_saved [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - lappend overlay_grapheme_control_list [list other $code] - } elseif {[regexp {\x1b8|\x1b\[u} $code]} { - #experiment - #cursor_restore - for the replays - set o_codestack [list $temp_cursor_saved] - lappend overlay_grapheme_control_list [list other $code] - } else { - #review - if {[punk::ansi::codetype::is_gx_open $code]} { - set o_gxstack [list "gx0_on"] - lappend overlay_grapheme_control_list [list gx0 gx0_on] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } elseif {[punk::ansi::codetype::is_gx_close $code]} { - set o_gxstack [list] - lappend overlay_grapheme_control_list [list gx0 gx0_off] ;#don't store code - will complicate debugging if we spit it out and jump character sets - } else { - lappend overlay_grapheme_control_list [list other $code] - } - } - } - } - - } - #replay code for last overlay position in input line - should take account of possible trailing sgr code after last grapheme - set max_overlay_grapheme_index [expr {$i_o -1}] - lappend overstacks $o_codestack - lappend overstacks_gx $o_gxstack - - #set replay_codes_overlay [join $o_codestack ""] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}$o_codestack] - - #if {[tcl::dict::exists $overstacks $max_overlay_grapheme_index]} { - # set replay_codes_overlay [join [tcl::dict::get $overstacks $max_overlay_grapheme_index] ""] - #} else { - # set replay_codes_overlay "" - #} - # -- --- --- --- --- --- --- --- - - - #potential problem - combinining diacritics directly following control chars like \r \b - - # -- --- --- - #we need to initialise overflow_idx before any potential row-movements - as they need to perform a loop break and force in_excess to 1 - if {$opt_expand_right} { - #expand_right true means we can have lines as long as we want, but either way there can be excess data that needs to be thrown back to the calling loop. - #we currently only support horizontal expansion to the right (review regarding RTL text!) - set overflow_idx -1 - } else { - #expand_right zero - we can't grow beyond our column width - so we get ellipsis or truncation - if {$opt_width ne "\uFFEF"} { - set overflow_idx [expr {$opt_width}] - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - } - # -- --- --- - - set outcols $undercols ;#leave undercols as is, outcols can potentially be appended to. - - set unapplied "" ;#if we break for move row (but not for /v ?) - set unapplied_list [list] - - set insert_lines_above 0 ;#return key - set insert_lines_below 0 - set instruction "" - - # -- --- --- - #cursor_save_dec, cursor_restore_dec etc - set cursor_restore_required 0 - set cursor_saved_attributes "" - set cursor_saved_position "" - # -- --- --- - - #set idx 0 ;# line index (cursor - 1) - #set idx [expr {$opt_colstart + $opt_colcursor} -1] - - #idx is the per column output index - set idx [expr {$opt_colcursor -1}] ;#don't use opt_colstart here - we have padded and won't start emitting until idx reaches opt_colstart-1 - #cursor_column is usually one above idx - but we have opt_colstart which is like a margin - todo: remove cursor_column from the following loop and calculate it's offset when breaking or at end. - #(for now we are incrementing/decrementing both in sync - which is a bit silly) - set cursor_column $opt_colcursor - - #idx_over is the per grapheme overlay index - set idx_over -1 - - - #movements only occur within the overlay range. - #an underlay is however not necessary.. e.g - #renderline -expand_right 1 "" data - - #set re_mode {\x1b\[\?([0-9]*)(h|l)} ;#e.g DECAWM - #set re_col_move {\x1b\[([0-9]*)(C|D|G)$} - #set re_row_move {\x1b\[([0-9]*)(A|B)$} - #set re_both_move {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)H$} ;# or "f" ? - #set re_vt_sequence {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)~$} - #set re_cursor_save {\x1b\[s$} ;#note probable incompatibility with DECSLRM (set left right margin)! - #set re_cursor_restore {\x1b\[u$} - #set re_cursor_save_dec {\x1b7$} - #set re_cursor_restore_dec {\x1b8$} - #set re_other_single {\x1b(D|M|E)$} - #set re_decstbm {\x1b\[([0-9]*)(?:;){0,1}([0-9]*)r$} ;#DECSTBM set top and bottom margins - - #puts "-->$overlay_grapheme_control_list<--" - #puts "-->overflow_idx: $overflow_idx" - for {set gci 0} {$gci < [llength $overlay_grapheme_control_list]} {incr gci} { - set gc [lindex $overlay_grapheme_control_list $gci] - lassign $gc type item - - #emit plaintext chars first using existing SGR codes from under/over stack as appropriate - #then check if the following code is a cursor movement within the line and adjust index if so - #foreach ch $overlay_graphemes {} - switch -- $type { - g { - set ch $item - #crm_mode affects both graphic and control - if {0 && $crm_mode} { - set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $ch] - set chars [string map [list \n "\x1b\[00001E"] $chars] - if {[llength [split $chars ""]] > 1} { - priv::render_unapplied $overlay_grapheme_control_list $gci - #prefix the unapplied controls with the string version of this control - set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]] - set unapplied [join $unapplied_list ""] - #incr idx_over - break - } else { - set ch $chars - } - } - incr idx_over; #idx_over (until unapplied reached anyway) is per *grapheme* in the overlay - not per col. - if {($idx < ($opt_colstart -1))} { - incr idx [grapheme_width_cached $ch] - continue - } - #set within_undercols [expr {$idx <= [llength $undercols]-1}] ;#within our active data width - set within_undercols [expr {$idx <= $renderwidth-1}] - - #https://www.enigma.com/resources/blog/the-secret-world-of-newline-characters - #\x85 NEL in the c1 control set is treated by some terminal emulators (e.g Hyper) as a newline, - #on some it's invisble but doesn't change the line, on some it's a visible glyph of width 1. - #This is hard to process in any standard manner - but I think the Hyper behaviour of doing what it was intended is perhaps most reasonable - #We will map it to the same behaviour as lf here for now... but we need also to consider the equivalent ANSI sequence: \x1bE - - set chtest [tcl::string::map [list \n \x85 \b \r \v \x7f ] $ch] - #puts --->chtest:$chtest - #specials - each shoud have it's own test of what to do if it happens after overflow_idx reached - switch -- $chtest { - "" { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - if {$idx == 0} { - #puts "---a at col 1" - #linefeed at column 1 - #leave the overflow_idx ;#? review - set instruction lf_start ;#specific instruction for newline at column 1 - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } elseif {$overflow_idx != -1 && $idx == $overflow_idx} { - #linefeed after final column - #puts "---c at overflow_idx=$overflow_idx" - incr cursor_row - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1 - set instruction lf_overflow ;#only special treatment is to give it it's own instruction in case caller needs to handle differently - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } else { - #linefeed occurred in middle or at end of text - #puts "---mid-or-end-text-linefeed idx:$idx overflow_idx:$overflow_idx" - if {$insert_mode == 0} { - incr cursor_row - if {$idx == -1 || $overflow_idx > $idx} { - #don't set overflow_idx higher if it's already set lower and we're adding graphemes to overflow - set overflow_idx $idx ;#override overflow_idx even if it was set to -1 due to expand_right = 1 - } - set instruction lf_mid - priv::render_unapplied $overlay_grapheme_control_list $gci - break - } else { - incr cursor_row - #don't adjust the overflow_idx - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction lf_mid - break ;# could have overdata following the \n - don't keep processing - } - } - - } - "" { - #will we want/need to use raw for keypresses in terminal? (terminal with LNM in standard reset mode means enter= this is the usual config for terminals) - #So far we are assuming the caller has translated to and handle above.. REVIEW. - - #consider also the old space-carriagereturn softwrap convention used in some terminals. - #In the context of rendering to a block of text - this works similarly in that the space gets eaten so programs emitting space-cr at the terminal width col will pretty much get what they expect. - set idx [expr {$opt_colstart -1}] - set cursor_column $opt_colstart ;#? - } - "" { - #literal backspace char - not necessarily from keyboard - #review - backspace effect on double-width chars - we are taking a column-editing perspective in overtype - #(important for -transparent option - hence replacement chars for half-exposed etc) - #review - overstrike support as per nroff/less (generally considered an old technology replaced by unicode mechanisms and/or ansi SGR) - if {$idx > ($opt_colstart -1)} { - incr idx -1 - incr cursor_column -1 - } else { - set flag 0 - if $flag { - #review - conflicting requirements? Need a different sequence for destructive interactive backspace? - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction backspace_at_start - break - } - } - } - "" { - #literal del character - some terminals send just this for what is generally expected to be a destructive backspace - #We instead treat this as a pure delete at current cursor position - it is up to the repl or terminal to remap backspace key to a sequence that has the desired effect. - priv::render_delchar $idx - } - "" { - #end processing this overline. rest of line is remainder. cursor for column as is. - #REVIEW - this theoretically depends on terminal's vertical tabulation setting (name?) - #e.g it could be configured to jump down 6 rows. - #On the other hand I've seen indications that some modern terminal emulators treat it pretty much as a linefeed. - #todo? - incr cursor_row - set overflow_idx $idx - #idx_over has already been incremented as this is both a movement-control and in some sense a grapheme - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction vt - break - } - default { - if {$overflow_idx != -1} { - #review - how to check arbitrary length item such as tab is going to overflow .. before we get to overflow_idx? - #call grapheme_width_cached on each ch, or look for tab specifically as it's currently the only known reason to have a grapheme width > 2? - #we need to decide what a tab spanning the overflow_idx means and how it affects wrap etc etc - if {$idx == $overflow_idx-1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 2} { - #review split 2w overflow? - #we don't want to make the decision here to split a 2w into replacement characters at end of line and beginning of next line - #better to consider the overlay char as unable to be applied to the line - #render empty column(?) - and reduce overlay grapheme index by one so that the current ch goes into unapplied - #throwing back to caller with instruction complicates its job - but is necessary to avoid making decsions for it here. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #change the overflow_idx - set overflow_idx $idx - incr idx - incr idx_over -1 ;#set overlay grapheme index back one so that sgr stack from previous overlay grapheme used - priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#note $gci-1 instead of just gci - #throw back to caller's loop - add instruction to caller as this is not the usual case - #caller may for example choose to render a single replacement char to this line and omit the grapheme, or wrap it to the next line - set instruction overflow_splitchar - break - } elseif {$owidth > 2} { - #? tab? - #TODO! - puts stderr "overtype::renderline long overtext grapheme '[ansistring VIEW -lf 1 -vt 1 $ch]' not handled" - #tab of some length dependent on tabstops/elastic tabstop settings? - } - } elseif {$idx >= $overflow_idx} { - #REVIEW - set next_gc [lindex $overlay_grapheme_control_list $gci+1] ;#next grapheme or control - lassign $next_gc next_type next_item - if {$autowrap_mode || $next_type ne "g"} { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci-1]] - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #don't incr idx beyond the overflow_idx - #idx_over already incremented - decrement so current overlay grapheme stacks go to unapplied - incr idx_over -1 - #priv::render_unapplied $overlay_grapheme_control_list [expr {$gci-1}] ;#back one index here too - priv::render_this_unapplied $overlay_grapheme_control_list $gci ;# - set instruction overflow - break - } else { - #no point throwing back to caller for each grapheme that is overflowing - #without this branch - renderline would be called with overtext reducing only by one grapheme per call - #processing a potentially long overtext each time (ie - very slow) - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #JMN4 - - } - } - } else { - #review. - #overflow_idx = -1 - #This corresponds to expand_right being true (at least until overflow_idx is in some cases forced to a value when throwing back to calling loop) - } - - if {($do_transparency && [regexp $opt_transparent $ch])} { - #pre opt_colstart is effectively transparent (we have applied padding of required number of columns to left of overlay) - if {$idx > [llength $outcols]-1} { - lappend outcols " " - #tcl::dict::set understacks $idx [list] ;#review - use idx-1 codestack? - lset understacks $idx [list] - incr idx - incr cursor_column - } else { - #todo - punk::char::char_width - set g [lindex $outcols $idx] - #JMN - set uwidth [grapheme_width_cached $g] - if {[lindex $outcols $idx] eq ""} { - #2nd col of 2-wide char in underlay - incr idx - incr cursor_column - } elseif {$uwidth == 0} { - #e.g control char ? combining diacritic ? - incr idx - incr cursor_column - } elseif {$uwidth == 1} { - set owidth [grapheme_width_cached $ch] - incr idx - incr cursor_column - if {$owidth > 1} { - incr idx - incr cursor_column - } - } elseif {$uwidth > 1} { - if {[grapheme_width_cached $ch] == 1} { - if {!$insert_mode} { - #normal singlewide transparent overlay onto double-wide underlay - set next_pt_overchar [tcl::string::index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay - if {$next_pt_overchar eq ""} { - #special-case trailing transparent - no next_pt_overchar - incr idx - incr cursor_column - } else { - if {[regexp $opt_transparent $next_pt_overchar]} { - incr idx - incr cursor_column - } else { - #next overlay char is not transparent.. first-half of underlying 2wide char is exposed - #priv::render_addchar $idx $opt_exposed1 [tcl::dict::get $overstacks $idx_over] [tcl::dict::get $overstacks_gx $idx_over] $insert_mode - priv::render_addchar $idx $opt_exposed1 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } else { - #? todo - decide what transparency even means for insert mode - incr idx - incr cursor_column - } - } else { - #2wide transparency over 2wide in underlay - review - incr idx - incr cursor_column - } - } - } - } else { - - set idxchar [lindex $outcols $idx] - #non-transparent char in overlay or empty cell - if {$idxchar eq "\u0000"} { - #empty/erased cell indicator - set uwidth 1 - } else { - set uwidth [grapheme_width_cached $idxchar] - } - if {$within_undercols} { - if {$idxchar eq ""} { - #2nd col of 2wide char in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 - #JMN - this has to expose if our startposn chopped an underlay - but not if we already overwrote the first half of the widechar underlay grapheme - #e.g renderline \uFF21\uFF21--- a\uFF23\uFF23 - #vs - # renderline -startcolumn 2 \uFF21---- \uFF23 - if {[lindex $outcols $idx-1] != ""} { - #verified it's an empty following a filled - so it's a legit underlay remnant (REVIEW - when would it not be??) - #reset previous to an exposed 1st-half - but leave understacks code as is - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 - } - incr idx - } else { - set prevcolinfo [lindex $outcols $idx-1] - #for insert mode - first replace the empty 2ndhalf char with exposed2 before shifting it right - #REVIEW - this leaves a replacement character permanently in our columns.. but it is consistent regarding length (?) - #The alternative is to disallow insertion at a column cursor that is at 2nd half of 2wide char - #perhaps by inserting after the char - this may be worthwhile - but may cause other surprises - #It is perhaps best avoided at another level and try to make renderline do exactly as it's told - #the advantage of this 2w splitting method is that the inserted character ends up in exactly the column we expect. - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 0 ;#replace not insert - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] 1 ;#insert - same index - if {$prevcolinfo ne ""} { - #we've split the 2wide - it may already have been rendered as an exposed1 - but not for example if our startcolumn was current idx - priv::render_addchar [expr {$idx-1}] $opt_exposed1 [lindex $understacks $idx-1] [lindex $understacks_gx $idx-1] 0 ;#replace not insert - } ;# else?? - incr idx - } - if {$cursor_column < [llength $outcols] || $overflow_idx == -1} { - incr cursor_column - } - } elseif {$uwidth == 0} { - #what if this is some other c0/c1 control we haven't handled specifically? - - #by emitting a preceding empty-string column - we associate whatever this char is with the preceeding non-zero-length character and any existing zero-lengths that follow it - #e.g combining diacritic - increment before over char REVIEW - #arguably the previous overchar should have done this - ie lookahead for combiners? - #if we can get a proper grapheme_split function - this should be easier to tidy up. - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column 2 - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } elseif {$uwidth == 1} { - #includes null empty cells - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - priv::render_addchar $idx "" [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #if next column in underlay empty - we've overwritten first half of underlying 2wide grapheme - #replace with rhs exposure in case there are no more overlay graphemes coming - use underlay's stack - if {([llength $outcols] >= $idx +2) && [lindex $outcols $idx+1] eq ""} { - priv::render_addchar [expr {$idx+1}] $opt_exposed2 [lindex $understacks $idx+1] [lindex $understacks_gx $idx+1] $insert_mode - } - incr idx - } - if {($cursor_column < [llength $outcols]) || $overflow_idx == -1} { - incr cursor_column - } - } elseif {$uwidth > 1} { - set owidth [grapheme_width_cached $ch] - if {$owidth == 1} { - #1wide over 2wide in underlay - if {!$insert_mode} { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - priv::render_addchar $idx $opt_exposed2 [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #don't incr idx - we are just putting a broken-indication in the underlay - which may get overwritten by next overlay char - } else { - #insert mode just pushes all to right - no exposition char here - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } else { - #2wide over 2wide - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx 2 - incr cursor_column 2 - } - - if {$cursor_column > [llength $outcols] && $overflow_idx != -1} { - set cursor_column [llength $outcols] - } - } - } else { - priv::render_addchar $idx $ch [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - incr idx - incr cursor_column - } - } - } - } ;# end switch - - - } - other - crmcontrol { - if {$crm_mode && $type ne "crmcontrol" && $item ne "\x1b\[00001E"} { - if {$item eq "\x1b\[3l"} { - set crm_mode 0 - } else { - #When our initial overlay split was done - we weren't in crm_mode - so there are codes that weren't mapped to unicode control character representations - #set within_undercols [expr {$idx <= $renderwidth-1}] - #set chars [ansistring VIEW -nul 1 -lf 2 -vt 2 -ff 2 $item] - set chars [ansistring VIEW -nul 1 -lf 1 -vt 1 -ff 1 $item] - priv::render_unapplied $overlay_grapheme_control_list $gci - #prefix the unapplied controls with the string version of this control - set unapplied_list [linsert $unapplied_list 0 {*}[split $chars ""]] - set unapplied [join $unapplied_list ""] - - break - } - } - - #todo - consider CSI s DECSLRM vs ansi.sys \x1b\[s - we need \x1b\[s for oldschool ansi art - but may have to enable only for that. - #we should possibly therefore reverse this mapping so that x1b7 x1b8 are the primary codes for save/restore? - set code [tcl::string::map [list \x1b7 \x1b\[s \x1b8 \x1b\[u ] $item] - #since this element isn't a grapheme - advance idx_over to next grapheme overlay when about to fill 'unapplied' - - - #remap of DEC cursor_save/cursor_restore from ESC sequence to equivalent CSI - #probably not ideal - consider putting cursor_save/cursor_restore in functions so they can be called from the appropriate switch branch instead of using this mapping - #review - cost/benefit of function calls within these switch-arms instead of inline code? - - set c1 [tcl::string::index $code 0] - set c1c2c3 [tcl::string::range $code 0 2] - #set re_ST_open {(?:\033P|\u0090|\033X|\u0098|\033\^|\u009e|\033_|\u009f)} - #tcl 8.7 - faster to use inline list than to store it in a local var outside of loop. - #(somewhat surprising) - set leadernorm [tcl::string::range [tcl::string::map [list\ - \x1b\[< 1006\ - \x1b\[ 7CSI\ - \x1bY 7MAP\ - \x1bP 7DCS\ - \x90 8DCS\ - \x9b 8CSI\ - \x1b\] 7OSC\ - \x9d 8OSC\ - \x1b 7ESC\ - ] $c1c2c3] 0 3] ;#leadernorm is 1st 1,2 or 3 chars mapped to 4char normalised indicator - or is original first chars (1,2 or 3 len) - - #we leave the tail of the code unmapped for now - switch -- $leadernorm { - 1006 { - #https://invisible-island.net/xterm/ctlseqs/ctlseqs.html - #SGR (1006) CSI < followed by colon separated encoded-button-value,px,py ordinates and final M for button press m for button release - set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 3 end]] - } - 7CSI - 7OSC { - #set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]] - set codenorm $leadernorm[tcl::string::range $code 2 end] - } - 7DCS { - #ESC P - #Device Control String https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Controls-beginning-with-ESC:ESC-F.C74 - set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 2 end]] - } - 8DCS { - #8-bit Device Control String - set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] - } - 7MAP { - #map to another type of code to share implementation branch - set codenorm $leadernorm[tcl::string::range $code 1 end] - } - 7ESC { - #set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] - set codenorm $leadernorm[tcl::string::range $code 1 end] - } - 8CSI - 8OSC { - set codenorm [tcl::string::cat $leadernorm [tcl::string::range $code 1 end]] - } - default { - puts stderr "Sequence detected as ANSI, but not handled in leadernorm switch. code: [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - #we haven't made a mapping for this - #could in theory be 1,2 or 3 in len - #although we shouldn't be getting here if the regexp for ansi codes is kept in sync with our switch branches - set codenorm $code - } - } - - switch -- $leadernorm { - 7MAP { - switch -- [lindex $codenorm 4] { - Y { - #vt52 movement. we expect 2 chars representing position (limited range) - set params [tcl::string::range $codenorm 5 end] - if {[tcl::string::length $params] != 2} { - #shouldn't really get here or need this branch if ansi splitting was done correctly - puts stderr "overtype::renderline ESC Y recognised as vt52 move, but incorrect parameters length ([string length $params] vs expected 2) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]" - } - set line [tcl::string::index $params 5] - set column [tcl::string::index $params 1] - set r [expr {[scan $line %c] -31}] - set c [expr {[scan $column %c] -31}] - - #MAP to: - #CSI n;m H - CUP - Cursor Position - set leadernorm 7CSI - set codenorm "$leadernorm${r}\;${c}H" - } - } - } - } - - #we've mapped 7 and 8bit escapes to values we can handle as literals in switch statements to take advantange of jump tables. - switch -- $leadernorm { - 1006 { - #TODO - # - switch -- [tcl::string::index $codenorm end] { - M { - puts stderr "mousedown $codenorm" - } - m { - puts stderr "mouseup $codenorm" - } - } - - } - {7CSI} - {8CSI} { - set param [tcl::string::range $codenorm 4 end-1] - #puts stdout "--> CSI [tcl::string::index $leadernorm 0] bit param:$param" - set code_end [tcl::string::index $codenorm end] ;#used for e.g h|l set/unset mode - - switch -exact -- $code_end { - A { - #Row move - up - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #todo - lassign [split $param {;}] num modifierkey - if {$modifierkey ne ""} { - puts stderr "modifierkey:$modifierkey" - } - - if {$num eq ""} {set num 1} - incr cursor_row -$num - - if {$cursor_row < 1} { - set cursor_row 1 - } - - #ensure rest of *overlay* is emitted to remainder - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up - #retain cursor_column - break - } - B { - #CUD - Cursor Down - #Row move - down - lassign [split $param {;}] num modifierkey - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move down - if {$modifierkey ne ""} { - puts stderr "modifierkey:$modifierkey" - } - if {$num eq ""} {set num 1} - incr cursor_row $num - - - incr idx_over ;#idx_over hasn't encountered a grapheme and hasn't advanced yet - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - C { - #CUF - Cursor Forward - #Col move - #puts stdout "->forward" - #todo - consider right-to-left cursor mode (e.g Hebrew).. some day. - #cursor forward - #right-arrow/move forward - lassign [split $param {;}] num modifierkey - if {$modifierkey ne ""} { - puts stderr "modifierkey:$modifierkey" - } - if {$num eq ""} {set num 1} - - #todo - retrict to moving 1 position past datalen? restrict to column width? - #should ideally wrap to next line when interactive and not on last row - #(some ansi art seems to expect this behaviour) - #This presumably depends on the terminal's wrap mode - #e.g DECAWM autowrap mode - # CSI ? 7 h - set: autowrap (also tput smam) - # CSI ? 7 l - reset: no autowrap (also tput rmam) - set version 2 - if {$version eq "2"} { - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$cursor_column == $max+1} { - #move_forward while in overflow - incr cursor_column -1 - } - - if {($cursor_column + $num) <= $max} { - incr idx $num - incr cursor_column $num - } else { - if {$autowrap_mode} { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #jmn - if {$idx == $overflow_idx} { - incr num - } - - #horizontal movement beyond line extent needs to wrap - throw back to caller - #we may have both overflow_right and unapplied data - #(can have overflow_right if we were in insert_mode and processed chars prior to this movement) - #leave row as is - caller will need to determine how many rows the column-movement has consumed - incr cursor_column $num ;#give our caller the necessary info as columns from start of row - #incr idx_over - #should be gci following last one applied - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmoveforward - break - } else { - set cursor_column $max - set idx [expr {$cursor_column -1}] - } - } - } else { - #review - dead branch - if {!$expand_right || ($cursor_column + $num) <= [llength $outcols+1]} { - incr idx $num - incr cursor_column $num - } else { - if {!$insert_mode} { - #block editing style with arrow keys - #overtype mode - set idxstart $idx - set idxend [llength $outcols] - set moveend [expr {$idxend - $idxstart}] - if {$moveend < 0} {set moveend 0} ;#sanity? - #puts "idxstart:$idxstart idxend:$idxend outcols[llength $outcols] undercols:[llength $undercols]" - incr idx $moveend - incr cursor_column $moveend - #if {[tcl::dict::exists $understacks $idx]} { - # set stackinfo [tcl::dict::get $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - #} else { - # set stackinfo [list] - #} - if {$idx < [llength $understacks]} { - set stackinfo [lindex $understacks $idx] ;#use understack at end - which may or may not have already been replaced by stack from overtext - } else { - set stackinfo [list] - } - if {$idx < [llength $understacks_gx]} { - #set gxstackinfo [tcl::dict::get $understacks_gx $idx] - set gxstackinfo [lindex $understacks_gx $idx] - } else { - set gxstackinfo [list] - } - #pad outcols - set movemore [expr {$num - $moveend}] - #assert movemore always at least 1 or we wouldn't be in this branch - for {set m 1} {$m <= $movemore} {incr m} { - incr idx - incr cursor_column - priv::render_addchar $idx " " $stackinfo $gxstackinfo $insert_mode - } - } else { - #normal - insert - incr idx $num - incr cursor_column $num - if {$idx > [llength $outcols]} { - set idx [llength $outcols];#allow one beyond - for adding character at end of line - set cursor_column [expr {[llength $outcols]+1}] - } - } - } - } - } - D { - #Col move - #puts stdout "<-back" - #cursor back - #left-arrow/move-back when ltr mode - lassign [split $param {;}] num modifierkey - if {$modifierkey ne ""} { - puts stderr "modifierkey:$modifierkey" - } - if {$num eq ""} {set num 1} - - set version 2 - if {$version eq "2"} { - #todo - startcolumn offset! - if {$cursor_column - $num >= 1} { - incr idx -$num - incr cursor_column -$num - } else { - if {!$autowrap_mode} { - set cursor_column 1 - set idx 0 - } else { - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr cursor_column -$num - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction wrapmovebackward - break - } - } - } else { - incr idx -$num - incr cursor_column -$num - if {$idx < $opt_colstart-1} { - #wrap to previous line and position cursor at end of data - set idx [expr {$opt_colstart-1}] - set cursor_column $opt_colstart - } - } - } - E { - #CNL - Cursor Next Line - if {$param eq ""} { - set downmove 1 - } else { - set downmove [expr {$param}] - } - puts stderr "renderline CNL down-by-$downmove" - set cursor_column 1 - set cursor_row [expr {$cursor_row + $downmove}] - set idx [expr {$cursor_column -1}] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - - } - F { - #CPL - Cursor Previous Line - if {$param eq ""} { - set upmove 1 - } else { - set upmove [expr {$param}] - } - puts stderr "renderline CPL up-by-$upmove" - set cursor_column 1 - set cursor_row [expr {$cursor_row -$upmove}] - if {$cursor_row < 1} { - set cursor_row 1 - } - set idx [expr {$cursor_column - 1}] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - - } - G { - #CHA - Cursor Horizontal Absolute (move to absolute column no) - if {$param eq ""} { - set targetcol 1 - } else { - set targetcol $param - if {![string is integer -strict $targetcol]} { - puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Unrecognised parameter '$param'" - } - set targetcol [expr {$param}] - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$targetcol > $max} { - puts stderr "renderline CHA (Cursor Horizontal Absolute) error. Param '$param' > max: $max" - set targetcol $max - } - } - #adjust to colstart - as column 1 is within overlay - #??? REVIEW - set idx [expr {($targetcol -1) + $opt_colstart -1}] - - - set cursor_column $targetcol - #puts stderr "renderline absolute col move ESC G (TEST)" - } - H - f { - #CSI n;m H - CUP - Cursor Position - - #CSI n;m f - HVP - Horizontal Vertical Position REVIEW - same as CUP with differences (what?) in some terminal modes - # - 'counts as effector format function (like CR or LF) rather than an editor function (like CUD or CNL)' - # - REVIEW - #see Annex A at: https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf - - #test e.g ansicat face_2.ans - #$re_both_move - lassign [split $param {;}] paramrow paramcol - #missing defaults to 1 - #CSI ;5H = CSI 1;5H -> row 1 col 5 - #CSI 17;H = CSI 17H = CSI 17;1H -> row 17 col 1 - - if {$paramcol eq ""} {set paramcol 1} - if {$paramrow eq ""} {set paramrow 1} - if {![string is integer -strict $paramcol] || ![string is integer -strict $paramrow]} { - puts stderr "renderline CUP (CSI H) unrecognised param $param" - #ignore? - } else { - set max [llength $outcols] - if {$overflow_idx == -1} { - incr max - } - if {$paramcol > $max} { - set target_column $max - } else { - set target_column [expr {$paramcol}] - } - - - if {$paramrow < 1} { - puts stderr "renderline CUP (CSI H) bad row target 0. Assuming 1" - set target_row 1 - } else { - set target_row [expr {$paramrow}] - } - if {$target_row == $cursor_row} { - #col move only - no need for break and move - #puts stderr "renderline CUP col move only to col $target_column param:$param" - set cursor_column $target_column - set idx [expr {$cursor_column -1}] - } else { - set cursor_row $target_row - set cursor_column $target_column - set idx [expr {$cursor_column -1}] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move - break - } - } - } - J { - set modegroup [tcl::string::index $codenorm 4] ;#e.g ? - switch -exact -- $modegroup { - ? { - #CSI ? Pn J - selective erase - puts stderr "overtype::renderline ED - SELECTIVE ERASE IN DISPLAY (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - default { - puts stderr "overtype::renderline ED - ERASE IN DISPLAY (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - if {$param eq ""} {set param 0} - switch -exact -- $param { - 0 { - #clear from cursor to end of screen - } - 1 { - #clear from cursor to beginning of screen - } - 2 { - #clear entire screen - #ansi.sys - move cursor to upper left REVIEW - set cursor_row 1 - set cursor_column 1 - set idx [expr {$cursor_column -1}] - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - incr idx_over - if {[llength $outcols]} { - priv::render_erasechar 0 [llength $outcols] - } - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction clear_and_move - break - } - 3 { - #clear entire screen. presumably cursor doesn't move - otherwise it would be same as 2J ? - - } - default { - } - } - - } - } - } - K { - #see DECECM regarding background colour - set modegroup [tcl::string::index $codenorm 4] ;#e.g ? - switch -exact -- $modegroup { - ? { - puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE (UNIMPLEMENTED) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - set param [string range $param 1 end] ;#chop qmark - if {$param eq ""} {set param 0} - switch -exact -- $param { - 0 { - #clear from cursor to end of line - depending on DECSCA - } - 1 { - #clear from cursor to beginning of line - depending on DECSCA - - } - 2 { - #clear entire line - depending on DECSCA - } - default { - puts stderr "overtype::renderline DECSEL - SELECTIVE ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - - } - default { - puts stderr "overtype::renderline EL - ERASE IN LINE (TESTING) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - if {$param eq ""} {set param 0} - switch -exact -- $param { - 0 { - #clear from cursor to end of line - } - 1 { - #clear from cursor to beginning of line - - } - 2 { - #clear entire line - } - default { - puts stderr "overtype::renderline EL - ERASE IN LINE PARAM '$param' unrecognised [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - } - } - } - L { - puts stderr "overtype::renderline IL - Insert Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - M { - #CSI Pn M - DL - Delete Line - puts stderr "overtype::renderline DL - Delete Line - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - - } - T { - #CSI Pn T - SD Pan Up (empty lines introduced at top) - #CSI Pn+T - kitty extension (lines at top come from scrollback buffer) - #Pn new lines appear at top of the display, Pn old lines disappear at the bottom of the display - if {$param eq "" || $param eq "0"} {set param 1} - if {[string index $param end] eq "+"} { - puts stderr "overtype::renderline CSI Pn + T - kitty Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } else { - puts stderr "overtype::renderline CSI Pn T - SD Pan Up - not implemented [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - X { - puts stderr "overtype::renderline X ECH ERASE CHARACTER - $param" - #ECH - erase character - if {$param eq "" || $param eq "0"} {set param 1}; #param=count of chars to erase - priv::render_erasechar $idx $param - #cursor position doesn't change. - } - q { - set code_secondlast [tcl::string::index $codenorm end-1] - switch -exact -- $code_secondlast { - {"} { - #DECSCA - Select Character Protection Attribute - #(for use with selective erase: DECSED and DECSEL) - set param [tcl::string::range $codenorm 4 end-2] - if {$param eq ""} {set param 0} - #TODO - store like SGR in stacks - replays? - switch -exact -- $param { - 0 - 2 { - #canerase - puts stderr "overtype::renderline - DECSCA canerase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - 1 { - #cannoterase - puts stderr "overtype::renderline - DECSCA cannoterase not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - default { - puts stderr "overtype::renderline DECSCA param '$param' not understood [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - - } - default { - puts stderr "overtype::renderline - CSI ... q not implemented - [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - - } - r { - #$re_decstbm - #https://www.vt100.net/docs/vt510-rm/DECSTBM.html - #This control function sets the top and bottom margins for the current page. You cannot perform scrolling outside the margins - lassign [split $param {;}] margin_top margin_bottom - - #todo - return these for the caller to process.. - puts stderr "overtype::renderline DECSTBM set top and bottom margin not implemented" - #Also moves the cursor to col 1 line 1 of the page - set cursor_column 1 - set cursor_row 1 - - incr idx_over - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction move ;#own instruction? decstbm? - break - } - s { - #code conflict between ansi emulation and DECSLRM - REVIEW - #ANSISYSSC (when no parameters) - like other terminals - essentially treat same as DECSC - # todo - when parameters - support DECSLRM instead - - if {$param ne ""} { - #DECSLRM - should only be recognised if DECLRMM is set (vertical split screen mode) - lassign [split $param {;} margin_left margin_right - puts stderr "overtype DECSLRM not yet supported - got [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - if {$margin_left eq ""} { - set margin_left 1 - } - set columns_per_page 80 ;#todo - set to 'page width (DECSCPP set columns per page)' - could be 132 or?? - if {$margin_right eq ""} { - set margin_right $columns_per_page - } - puts stderr "DECSLRM margin left: $margin_left margin right: $margin_right" - if {![string is integer -strict $margin_left] || $margin_left < 0} { - puts stderr "DECSLRM invalid margin_left" - } - if {![string is integer -strict $margin_right] || $margin_right < 0} { - puts stderr "DECSLRM invalid margin_right" - } - set scrolling_region_size [expr {$margin_right - $margin_left}] - if {$scrolling_region_size < 2 || $scrolling_region_size > $columns_per_page} { - puts stderr "DECSLRM region size '$scrolling_regsion_size' must be between 1 and $columns_per_page" - } - #todo - - - } else { - #DECSC - #//notes on expected behaviour: - #DECSC - saves following items in terminal's memory - #cursor position - #character attributes set by the SGR command - #character sets (G0,G1,G2 or G3) currently in GL and GR - #Wrap flag (autowrap or no autowrap) - #State of origin mode (DECOM) - #selective erase attribute - #any single shift 2 (SS2) or single shift 3(SSD) functions sent - - #$re_cursor_save - #cursor save could come after last column - if {$overflow_idx != -1 && $idx == $overflow_idx} { - #bartman2.ans test file - fixes misalignment at bottom of dialog bubble - #incr cursor_row - #set cursor_column 1 - #bwings1.ans test file - breaks if we actually incr cursor (has repeated saves) - set cursor_saved_position [list row [expr {$cursor_row+1}] column 1] - } else { - set cursor_saved_position [list row $cursor_row column $cursor_column] - } - #there may be overlay stackable codes emitted that aren't in the understacks because they come between the last emmited character and the cursor_save control. - #we need the SGR and gx overlay codes prior to the cursor_save - - #a real terminal would not be able to know the state of the underlay.. so we should probably ignore it. - #set sgr_stack [lindex $understacks $idx] - #set gx_stack [lindex $understacks_gx $idx] ;#not actually a stack - just a boolean state (for now?) - - set sgr_stack [list] - set gx_stack [list] - - #we shouldn't need to scan for intermediate cursor save/restores - as restores would throw-back to the calling loop - so our overlay 'line' is since those. - #The overlay_grapheme_control_list had leading resets from previous lines - so we go back to the beginning not just the first grapheme. - - foreach gc [lrange $overlay_grapheme_control_list 0 $gci-1] { - lassign $gc type code - #types g other sgr gx0 - switch -- $type { - gx0 { - #code is actually a stand-in for the graphics on/off code - not the raw code - #It is either gx0_on or gx0_off - set gx_stack [list $code] - } - sgr { - #code is the raw code - if {[punk::ansi::codetype::is_sgr_reset $code]} { - #jmn - set sgr_stack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set sgr_stack [list $code] - lappend overlay_grapheme_control_list [list sgr $code] - } elseif {[priv::is_sgr $code]} { - #often we don't get resets - and codes just pile up. - #as a first step to simplifying - at least remove earlier straight up dupes - set dup_posns [lsearch -all -exact $sgr_stack $code] ;#needs -exact - codes have square-brackets (glob chars) - set sgr_stack [lremove $sgr_stack {*}$dup_posns] - lappend sgr_stack $code - } - } - } - } - set cursor_saved_attributes "" - switch -- [lindex $gx_stack 0] { - gx0_on { - append cursor_saved_attributes "\x1b(0" - } - gx0_off { - append cursor_saved_attributes "\x1b(B" - } - } - #append cursor_saved_attributes [join $sgr_stack ""] - append cursor_saved_attributes [punk::ansi::codetype::sgr_merge_list {*}$sgr_stack] - - #as there is apparently only one cursor storage element we don't need to throw back to the calling loop for a save. - - #don't incr index - or the save will cause cursor to move to the right - #carry on - } - } - u { - #ANSISYSRC save cursor (when no parameters) (DECSC) - - #$re_cursor_restore - #we are going to jump somewhere.. for now we will assume another line, and process accordingly. - #The caller has the cursor_saved_position/cursor_saved_attributes if any (?review - if we always pass it back it, we could save some calls for moves in same line) - #don't set overflow at this point. The existing underlay to the right must be preserved. - #we only want to jump and render the unapplied at the new location. - - #lset overstacks $idx_over [list] - #set replay_codes_overlay "" - - #if {$cursor_saved_attributes ne ""} { - # set replay_codes_overlay $cursor_saved_attributes ;#empty - or last save if it happend in this input chunk - #} else { - #jj - #set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - set replay_codes_overlay "" - #} - - #like priv::render_unapplied - but without the overlay's ansi reset or gx stacks from before the restore code - incr idx_over - - set unapplied "" - set unapplied_list [list] - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - #incr idx_over - } - set unapplied [join $unapplied_list ""] - #if the save occured within this line - that's ok - it's in the return value list and caller can prepend for the next loop. - set instruction restore_cursor - break - } - "{" { - - puts stderr "renderline warning - CSI.. - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" - } - "}" { - set code_secondlast [tcl::string::index $codenorm end-1] - switch -exact -- $code_secondlast { - ' { - puts stderr "renderline warning - DECIC - Insert Column - CSI...' - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" - } - default { - puts stderr "renderline warning - CSI.. - unimplemented [ansistring VIEW -lf 1 -nul 1 $code]" - } - } - } - ~ { - set code_secondlast [tcl::string::index $codenorm end-1] ;#used for e.g CSI x '~ - switch -exact -- $code_secondlast { - ' { - #DECDC - editing sequence - Delete Column - puts stderr "renderline warning - DECDC - unimplemented" - } - default { - #$re_vt_sequence - lassign [split $param {;}] key mod - - #Note that f1 to f4 show as ESCOP|Q|R|S (VT220?) but f5+ show as ESC\[15~ - # - #e.g esc \[2~ insert esc \[2;2~ shift-insert - #mod - subtract 1, and then use bitmask - #shift = 1, (left)Alt = 2, control=4, meta=8 (meta seems to do nothing on many terminals on windows? Intercepted by windows?) - #puts stderr "vt key:$key mod:$mod code:[ansistring VIEW $code]" - if {$key eq "1"} { - #home - } elseif {$key eq "2"} { - #Insert - if {$mod eq ""} { - #no modifier key - set insert_mode [expr {!$insert_mode}] - #rather than set the cursor - we return the insert mode state so the caller can decide - } - } elseif {$key eq "3"} { - #Delete - presumably this shifts other chars in the line, with empty cells coming in from the end - switch -- $mod { - "" { - priv::render_delchar $idx - } - "5" { - #ctrl-del - delete to end of word (pwsh) - possibly word on next line if current line empty(?) - } - } - } elseif {$key eq "4"} { - #End - } elseif {$key eq "5"} { - #pgup - } elseif {$key eq "6"} { - #pgDn - } elseif {$key eq "7"} { - #Home - #?? - set idx [expr {$opt_colstart -1}] - set cursor_column 1 - } elseif {$key eq "8"} { - #End - } elseif {$key eq "11"} { - #F1 - or ESCOP or e.g shift F1 ESC\[1;2P - } elseif {$key eq "12"} { - #F2 - or ESCOQ - } elseif {$key eq "13"} { - #F3 - or ESCOR - } elseif {$key eq "14"} { - #F4 - or ESCOS - } elseif {$key eq "15"} { - #F5 or shift F5 ESC\[15;2~ - } elseif {$key eq "17"} { - #F6 - } elseif {$key eq "18"} { - #F7 - } elseif {$key eq "19"} { - #F8 - } elseif {$key eq "20"} { - #F9 - } elseif {$key eq "21"} { - #F10 - } elseif {$key eq "23"} { - #F11 - } elseif {$key eq "24"} { - #F12 - } - - } - } - - } - h - l { - #set mode unset mode - #we are matching only last char to get to this arm - but are there other sequences ending in h|l we need to handle? - - #$re_mode if first after CSI is "?" - #some docs mention ESC=h|l - not seen on windows terminals.. review - #e.g https://www2.math.upenn.edu/~kazdan/210/computer/ansi.html - set modegroup [tcl::string::index $codenorm 4] ;#e.g ? = - switch -exact -- $modegroup { - ? { - set smparams [tcl::string::range $codenorm 5 end-1] ;#params between ? and h|l - #one or more modes can be set - set smparam_list [split $smparams {;}] - foreach num $smparam_list { - switch -- $num { - "" { - #ignore empties e.g extra/trailing semicolon in params - } - 5 { - #DECSNM - reverse video - #How we simulate this to render within a block of text is an open question. - #track all SGR stacks and constantly flip based on the current SGR reverse state? - #It is the job of the calling loop to do this - so at this stage we'll just set the states - - if {$code_end eq "h"} { - #set (enable) - set reverse_mode 1 - } else { - #reset (disable) - set reverse_mode 0 - } - - } - 7 { - #DECAWM autowrap - if {$code_end eq "h"} { - #set (enable) - set autowrap_mode 1 - if {$opt_width ne "\uFFEF"} { - set overflow_idx $opt_width - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - #review - can idx ever be beyond overflow_idx limit when we change e.g with a width setting and cursor movements? - # presume not usually - but sanity check with warning for now. - if {$idx >= $overflow_idx} { - puts stderr "renderline warning - idx '$idx' >= overflow_idx '$overflow_idx' - unexpected" - } - } else { - #reset (disable) - set autowrap_mode 0 - #REVIEW! - set overflow_idx -1 - } - } - 25 { - if {$code_end eq "h"} { - #visible cursor - - } else { - #invisible cursor - - } - } - 117 { - #DECECM - Erase Color Mode - #https://invisible-island.net/ncurses/ncurses.faq.html - #The Erase color selection controls the background color used when text is erased or new - #text is scrolled on to the screen. Screen background causes newly erased areas or - #scrolled text to be written using color index zero, the screen background. This is VT - #and DECterm compatible. Text background causes erased areas or scrolled text to be - #written using the current text background color. This is PC console compatible and is - #the factory default. - - #see also: https://unix.stackexchange.com/questions/251726/clear-to-end-of-line-uses-the-wrong-background-color-in-screen - } - } - } - } - = { - set num [tcl::string::range $codenorm 5 end-1] ;#param between = and h|l - puts stderr "overtype::renderline CSI=...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - default { - #e.g CSI 4 h - set num [tcl::string::range $codenorm 4 end-1] ;#param before h|l - switch -exact -- $num { - 3 { - puts stderr "CRM MODE $code_end" - #CRM - Show control character mode - # 'No control functions are executed except LF,FF and VT which are represented in the CRM FONT before a CRLF(new line) is executed' - # - #use ansistring VIEW -nul 1 -lf 2 -ff 2 -vt 2 - #https://vt100.net/docs/vt510-rm/CRM.html - #NOTE - vt100 CRM always does auto-wrap at right margin. - #disabling auto-wrap in set-up or by sequence is disabled. - #We should default to turning off auto-wrap when crm_mode enabled.. but - #displaying truncated (on rhs) crm can still be very useful - and we have optimisation in overflow to avoid excess renderline calls (per grapheme) - #we therefore could reasonably put in an exception to allow auto_wrap to be disabled after crm_mode is engaged, - #although this would be potentially an annoying difference to some.. REVIEW - if {$code_end eq "h"} { - set crm_mode 1 - set autowrap_mode 1 - if {$opt_width ne "\uFFEF"} { - set overflow_idx $opt_width - } else { - #review - this is also the cursor position when adding a char at end of line? - set overflow_idx [expr {[llength $undercols]}] ;#index at which we would be *in* overflow a row move may still override it - } - } else { - set crm_mode 0 - } - } - 4 { - #IRM - Insert/Replace Mode - if {$code_end eq "h"} { - #CSI 4 h - set insert_mode 1 - } else { - #CSI 4 l - #replace mode - set insert_mode 0 - } - } - default { - puts stderr "overtype::renderline CSI...h|l code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - } - } - | { - switch -- [tcl::string::index $codenorm end-1] { - {$} { - #CSI ... $ | DECSCPP set columns per page- (recommended in vt510 docs as preferable to DECCOLM) - #real terminals generally only supported 80/132 - #some other virtuals support any where from 2 to 65,536? - #we will allow arbitrary widths >= 2 .. to some as yet undetermined limit. - #CSI $ | - #empty or 0 param is 80 for compatibility - other numbers > 2 accepted - set page_width -1 ;#flag as unset - if {$param eq ""} { - set page_width 80 - } elseif {[string is integer -strict $param] && $param >=2 0} { - set page_width [expr {$param}] ;#we should allow leading zeros in the number - but lets normalize using expr - } else { - puts stderr "overtype::renderline unacceptable DECSPP value '$param'" - } - - if {$page_width > 2} { - puts stderr "overtype::renderline DECSCPP - not implemented - but selected width '$page_width' looks ok" - #if cursor already beyond new page_width - will move to right colum - otherwise no cursor movement - - } - - } - default { - puts stderr "overtype::renderline unrecognised CSI code ending in pipe (|) [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - default { - puts stderr "overtype::renderline CSI code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented" - } - } - } - 7ESC { - # - #re_other_single {\x1b(D|M|E)$} - #also vt52 Y.. - #also PM \x1b^...(ST) - switch -- [tcl::string::index $codenorm 4] { - c { - #RIS - reset terminal to initial state - where 'terminal' in this case is the renderspace - not the underlying terminal! - puts stderr "renderline reset" - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction reset - break - } - D { - #\x84 - #index (IND) - #vt102-docs: "Moves cursor down one line in same column. If cursor is at bottom margin, screen performs a scroll-up" - puts stderr "renderline ESC D not fully implemented" - incr cursor_row - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction down - #retain cursor_column - break - } - E { - #\x85 - #review - is behaviour different to lf? - #todo - possibly(?) same logic as handling above. i.e return instruction depends on where column_cursor is at the time we get NEL - #leave implementation until logic for is set in stone... still under review - #It's arguable NEL is a pure cursor movement as opposed to the semantic meaning of crlf or lf in a file. - # - #Next Line (NEL) "Move the cursor to the left margin on the next line. If the cursor is at the bottom margin, scroll the page up" - puts stderr "overtype::renderline ESC E unimplemented" - - } - H { - #\x88 - #Tab Set - puts stderr "overtype::renderline ESC H tab set unimplemented" - } - M { - #\x8D - #Reverse Index (RI) - #vt102-docs: "Moves cursor up one line in same column. If cursor is at top margin, screen performs a scroll-down" - puts stderr "overtype::renderline ESC M not fully implemented" - - set replay_codes_overlay [punk::ansi::codetype::sgr_merge_list {*}[lindex $overlay_grapheme_control_stacks $gci]] - #move up - incr cursor_row -1 - if {$cursor_row < 1} { - set cursor_row 1 - } - #ensure rest of *overlay* is emitted to remainder - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction up ;#need instruction for scroll-down? - #retain cursor_column - break - } - N { - #\x8e - affects next character only - puts stderr "overtype::renderline single shift select G2 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - O { - #\x8f - affects next character only - puts stderr "overtype::renderline single shift select G3 command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - P { - #\x90 - #DCS - shouldn't get here - handled in 7DCS branch - #similarly \] OSC (\x9d) and \\ (\x9c) ST - } - V { - #\x96 - - } - W { - #\x97 - } - X { - #\x98 - #SOS - if {[string index $code end] eq "\007"} { - set sos_content [string range $code 2 end-1] ;#ST is \007 - } else { - set sos_content [string range $code 2 end-2] ;#ST is \x1b\\ - } - #return in some useful form to the caller - #TODO! - lappend sos_list [list string $sos_content row $cursor_row column $cursor_column] - puts stderr "overtype::renderline ESCX SOS UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - ^ { - #puts stderr "renderline PM" - #Privacy Message. - if {[string index $code end] eq "\007"} { - set pm_content [string range $code 2 end-1] ;#ST is \007 - } else { - set pm_content [string range $code 2 end-2] ;#ST is \x1b\\ - } - #We don't want to render it - but we need to make it available to the application - #see the textblock library in punk, for the exception we make here for single backspace. - #It is unlikely to be encountered as a useful PM - so we hack to pass it through as a fix - #for spacing issues on old terminals which miscalculate the single-width 'Symbols for Legacy Computing' - if {$pm_content eq "\b"} { - #puts stderr "renderline PM sole backspace special handling for \U1FB00 - \U1FBFF" - #esc^\b\007 or esc^\besc\\ - #HACKY pass-through - targeting terminals that both mis-space legacy symbols *and* don't support PMs - #The result is repair of the extra space. If the terminal is a modern one and does support PM - the \b should be hidden anyway. - #If the terminal has the space problem AND does support PMs - then this just won't fix it. - #The fix relies on the symbol-supplier to cooperate by appending esc^\b\esc\\ to the problematic symbols. - - #priv::render_addchar $idx $code [lindex $overstacks $idx_over] [lindex $overstacks_gx $idx_over] $insert_mode - #idx has been incremented after last grapheme added - priv::render_append_to_char [expr {$idx -1}] $code - } - #lappend to a dict element in the result for application-specific processing - lappend pm_list $pm_content - } - _ { - #APC Application Program Command - #just warn for now.. - puts stderr "overtype::renderline ESC_ APC command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - default { - puts stderr "overtype::renderline ESC code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code] not implemented codenorm:[ansistring VIEW -lf 1 -vt 1 -nul 1 $codenorm]" - } - } - - } - 7DCS - 8DCS { - puts stderr "overtype::renderline DCS - DEVICE CONTROL STRING command UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - #ST (string terminator) \x9c or \x1b\\ - if {[tcl::string::index $codenorm end] eq "\x9c"} { - set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is 8-bit 0x9c - } else { - set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\ - } - - } - 7OSC - 8OSC { - # OSCs are terminated with ST of either \007 or \x1b\\ - we allow either whether code was 7 or 8 bit - if {[tcl::string::index $codenorm end] eq "\007"} { - set code_content [tcl::string::range $codenorm 4 end-1] ;#ST is \007 - } else { - set code_content [tcl::string::range $codenorm 4 end-2] ;#ST is \x1b\\ - } - set first_colon [tcl::string::first {;} $code_content] - if {$first_colon == -1} { - #there probably should always be a colon - but we'll try to make sense of it without - set osc_code $code_content ;#e.g \x1b\]104\007 vs \x1b\]104\;\007 - } else { - set osc_code [tcl::string::range $code_content 0 $first_colon-1] - } - switch -exact -- $osc_code { - 2 { - set newtitle [tcl::string::range $code_content 2 end] - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction [list set_window_title $newtitle] - break - } - 4 { - #OSC 4 - set colour palette - #can take multiple params - #e.g \x1b\]4\;1\;red\;2\;green\x1b\\ - set params [tcl::string::range $code_content 2 end] ;#strip 4 and first semicolon - set cmap [dict create] - foreach {cnum spec} [split $params {;}] { - if {$cnum >= 0 and $cnum <= 255} { - #todo - parse spec from names like 'red' to RGB - #todo - accept rgb:ab/cd/ef as well as rgb:/a/b/c (as alias for aa/bb/cc) - #also - what about rgb:abcd/defg/hijk and 12-bit abc/def/ghi ? - dict set cmap $cnum $spec - } else { - #todo - log - puts stderr "overtype::renderline OSC 4 set colour palette - bad color number: $cnum must be from 0 to 255. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - - puts stderr "overtype::renderline OSC 4 set colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - - - } - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 { - #OSC 10 through 17 - so called 'dynamic colours' - #can take multiple params - each successive parameter changes the next colour in the list - #- e.g if code started at 11 - next param is for 12. 17 takes only one param because there are no more - #10 change text foreground colour - #11 change text background colour - #12 change text cursor colour - #13 change mouse foreground colour - #14 change mouse background colour - #15 change tektronix foreground colour - #16 change tektronix background colour - #17 change highlight colour - set params [tcl::string::range $code_content 2 end] - - puts stderr "overtype::renderline OSC $osc_code set dynamic colours unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - - - } - 18 { - #why is this not considered one of the dynamic colours above? - #https://www.xfree86.org/current/ctlseqs.html - #tektronix cursor color - puts stderr "overtype::renderline OSC 18 - set tektronix cursor color unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - 99 { - #kitty desktop notifications - #https://sw.kovidgoyal.net/kitty/desktop-notifications/ - # 99 ; metadata ; payload - puts stderr "overtype::renderline OSC 99 kitty desktop notification unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - 104 { - #reset colour palette - #we want to do it for the current rendering context only (vvt) - not just pass through to underlying vt - puts stderr "overtype::renderline OSC 104 reset colour palette unimplemented. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - priv::render_unapplied $overlay_grapheme_control_list $gci - set instruction [list reset_colour_palette] - break - } - 1337 { - #iterm2 graphics and file transfer - puts stderr "overtype::renderline OSC 1337 iterm2 graphics/file_transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]" - } - 5113 { - puts stderr "overtype::renderline OSC 5113 kitty file transfer unimplemented. 1st 100 chars of code [ansistring VIEW -lf 1 -vt 1 -nul 1 [string range $code 0 99]]" - } - default { - puts stderr "overtype::renderline OSC - UNIMPLEMENTED. code [ansistring VIEW -lf 1 -vt 1 -nul 1 $code]" - } - } - - } - default { - } - } - - - } - default { - #don't need to handle sgr or gx0 types - #we have our sgr gx0 codes already in stacks for each overlay grapheme - } - } - } - - #-------- - if {$opt_expand_right == 0} { - #need to truncate to the width of the original undertext - #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok? - #set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars - } - if {$overflow_idx == -1} { - #overflow was initially unlimited and hasn't been overridden - } else { - - } - #-------- - - - #coalesce and replay codestacks for outcols grapheme list - set outstring "" ;#output prior to overflow - set overflow_right "" ;#remainder after overflow point reached - set i 0 - set cstack [list] - set prevstack [list] - set prev_g0 [list] - #note overflow_idx may already have been set lower if we had a row move above due to \v or ANSI moves - set in_overflow 0 ;#used to stop char-width scanning once in overflow - if {$overflow_idx == 0} { - #how does caller avoid an infinite loop if they have autowrap on and keep throwing graphemes to the next line? REVIEW - set in_overflow 1 - } - set trailing_nulls 0 - foreach ch [lreverse $outcols] { - if {$ch eq "\u0000"} { - incr trailing_nulls - } else { - break - } - } - if {$trailing_nulls} { - set first_tail_null_posn [expr {[llength $outcols] - $trailing_nulls}] - } else { - set first_tail_null_posn -1 - } - - #puts stderr "first_tail_null_posn: $first_tail_null_posn" - #puts stderr "colview: [ansistring VIEW $outcols]" - - foreach ch $outcols { - #puts "---- [ansistring VIEW $ch]" - - set gxleader "" - if {$i < [llength $understacks_gx]} { - #set g0 [tcl::dict::get $understacks_gx $i] - set g0 [lindex $understacks_gx $i] - if {$g0 ne $prev_g0} { - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } else { - set gxleader "\x1b(B" - } - } - set prev_g0 $g0 - } else { - set prev_g0 [list] - } - - set sgrleader "" - if {$i < [llength $understacks]} { - #set cstack [tcl::dict::get $understacks $i] - set cstack [lindex $understacks $i] - if {$cstack ne $prevstack} { - if {[llength $prevstack] && ![llength $cstack]} { - #This reset is important e.g testfile fruit.ans - we get overhang on rhs without it. But why is cstack empty? - append sgrleader \033\[m - } else { - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - } - } - set prevstack $cstack - } else { - set prevstack [list] - } - - - - if {$in_overflow} { - if {$i == $overflow_idx} { - set 0 [lindex $understacks_gx $i] - set gxleader "" - if {$g0 eq [list "gx0_on"]} { - set gxleader "\x1b(0" - } elseif {$g0 eq [list "gx0_off"]} { - set gxleader "\x1b(B" - } - append overflow_right $gxleader - set cstack [lindex $understacks $i] - set sgrleader "" - #whether cstack is same or differs from previous char's stack - we must have an output at the start of the overflow_right - #if {[llength $prevstack] && ![llength $cstack]} { - # append sgrleader \033\[m - #} - append sgrleader [punk::ansi::codetype::sgr_merge_list {*}$cstack] - append overflow_right $sgrleader - append overflow_right $ch - } else { - append overflow_right $gxleader - append overflow_right $sgrleader - append overflow_right $ch - } - } else { - if {$overflow_idx != -1 && $i+1 == $overflow_idx} { - #one before overflow - #will be in overflow in next iteration - set in_overflow 1 - if {[grapheme_width_cached $ch]> 1} { - #we overflowed with second-half of a double-width char - replace first-half with user-supplied exposition char (should be 1 wide) - set ch $opt_exposed1 - } - } - append outstring $gxleader - append outstring $sgrleader - if {$ch eq "\u0000"} { - if {$cp437_glyphs} { - #map all nulls including at tail to space - append outstring " " - } else { - if {$trailing_nulls && $i < $first_tail_null_posn} { - append outstring " " ;#map inner nulls to space - } else { - append outstring \u0000 - } - } - } else { - append outstring $ch - } - } - incr i - } - #flower.ans good test for null handling - reverse line building - #review - presence of overflow_right doesn't indicate line's trailing nulls should remain. - #The cells could have been erased? - #if {!$cp437_glyphs} { - # #if {![ansistring length $overflow_right]} { - # # set outstring [tcl::string::trimright $outstring "\u0000"] - # #} - # set outstring [tcl::string::trimright $outstring "\u0000"] - # set outstring [tcl::string::map {\u0000 " "} $outstring] - #} - - - #REVIEW - #set overflow_right [tcl::string::trimright $overflow_right "\u0000"] - #set overflow_right [tcl::string::map {\u0000 " "} $overflow_right] - - set replay_codes "" - if {[llength $understacks] > 0} { - if {$overflow_idx == -1} { - #set tail_idx [tcl::dict::size $understacks] - set tail_idx [llength $understacks] - } else { - set tail_idx [llength $undercols] - } - if {$tail_idx-1 < [llength $understacks]} { - #set replay_codes [join [lindex $understacks $tail_idx-1] ""] ;#tail replay codes - set replay_codes [punk::ansi::codetype::sgr_merge_list {*}[lindex $understacks $tail_idx-1]] ;#tail replay codes - } - if {$tail_idx-1 < [llength $understacks_gx]} { - set gx0 [lindex $understacks_gx $tail_idx-1] - if {$gx0 eq [list "gx0_on"]} { - #if it was on, turn gx0 off at the point we stop processing overlay - append outstring "\x1b(B" - } - } - } - if {[string length $overflow_right]} { - #puts stderr "remainder:$overflow_right" - } - #pdict $understacks - - if {[punk::ansi::ta::detect_sgr $outstring]} { - append outstring [punk::ansi::a] ;#without this - we would get for example, trailing backgrounds after rightmost column - - #close off any open gx? - #probably should - and overflow_right reopen? - } - - if {$opt_returnextra} { - #replay_codes is the codestack at the boundary - used for ellipsis colouring to match elided text - review - #replay_codes_underlay is the set of codes in effect at the very end of the original underlay - - #review - #replay_codes_overlay is the set of codes in effect at the very end of the original overlay (even if not all overlay was applied) - #todo - replay_codes for gx0 mode - - #overflow_idx may change during ansi & character processing - if {$overflow_idx == -1} { - set overflow_right_column "" - } else { - set overflow_right_column [expr {$overflow_idx+1}] - } - set result [tcl::dict::create\ - result $outstring\ - visualwidth [punk::ansi::printing_length $outstring]\ - instruction $instruction\ - stringlen [string length $outstring]\ - overflow_right_column $overflow_right_column\ - overflow_right $overflow_right\ - unapplied $unapplied\ - unapplied_list $unapplied_list\ - insert_mode $insert_mode\ - autowrap_mode $autowrap_mode\ - crm_mode $crm_mode\ - reverse_mode $reverse_mode\ - insert_lines_above $insert_lines_above\ - insert_lines_below $insert_lines_below\ - cursor_saved_position $cursor_saved_position\ - cursor_saved_attributes $cursor_saved_attributes\ - cursor_column $cursor_column\ - cursor_row $cursor_row\ - expand_right $opt_expand_right\ - replay_codes $replay_codes\ - replay_codes_underlay $replay_codes_underlay\ - replay_codes_overlay $replay_codes_overlay\ - pm_list $pm_list\ - ] - if {$opt_returnextra == 1} { - #puts stderr "renderline: $result" - return $result - } else { - #human/debug - map special chars to visual glyphs - set viewop VIEW - switch -- $opt_returnextra { - 2 { - #codes and character data - set viewop VIEWCODES ;#ansi colorisation of codes - green for SGR, blue/blue reverse for cursor_save/cursor_restore, cyan for movements, orange for others - } - 3 { - set viewop VIEWSTYLE ;#ansi colorise the characters within the output with preceding codes, stacking codes only within each dict value - may not be same SGR effect as the effect in-situ. - } - } - tcl::dict::set result result [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result result]] - tcl::dict::set result overflow_right [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result overflow_right]] - tcl::dict::set result unapplied [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied]] - tcl::dict::set result unapplied_list [ansistring VIEW -lf 1 -vt 1 [tcl::dict::get $result unapplied_list]] - tcl::dict::set result replay_codes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes]] - tcl::dict::set result replay_codes_underlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_underlay]] - tcl::dict::set result replay_codes_overlay [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result replay_codes_overlay]] - tcl::dict::set result cursor_saved_attributes [ansistring $viewop -lf 1 -vt 1 [tcl::dict::get $result cursor_saved_attributes]] - return $result - } - } else { - #puts stderr "renderline returning: result $outstring instruction $instruction unapplied $unapplied overflow_right $overflow_right" - return $outstring - } - #return [join $out ""] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace overtype ---}] -} - -tcl::namespace::eval overtype::piper { - proc overcentre {args} { - if {[llength $args] < 2} { - error {usage: ?-bias left|right? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::centre {*}$argsflags $under $over - } - proc overleft {args} { - if {[llength $args] < 2} { - error {usage: ?-startcolumn ? ?-transparent [0|1|]? ?-exposed1 ? ?-exposed2 ? ?-overflow [1|0]? overtext pipelinedata} - } - lassign [lrange $args end-1 end] over under - set argsflags [lrange $args 0 end-2] - tailcall overtype::left {*}$argsflags $under $over - } -} - - -# -- --- --- --- --- --- --- --- --- --- --- -proc overtype::transparentline {args} { - foreach {under over} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - set defaults [tcl::dict::create\ - -transparent 1\ - -exposed 1 " "\ - -exposed 2 " "\ - ] - set newargs [tcl::dict::merge $defaults $argsflags] - tailcall overtype::renderline {*}$newargs $under $over -} -#renderline may not make sense as it is in the long run for blocks of text - but is handy in the single-line-handling form anyway. -# We are trying to handle ansi codes in a block of text which is acting like a mini-terminal in some sense. -#We can process standard cursor moves such as \b \r - but no way to respond to other cursor movements e.g moving to other lines. -# -tcl::namespace::eval overtype::piper { - proc renderline {args} { - if {[llength $args] < 2} { - error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? overtext pipelinedata} - } - foreach {over under} [lrange $args end-1 end] break - set argsflags [lrange $args 0 end-2] - tailcall overtype::renderline {*}$argsflags $under $over - } -} -interp alias "" piper_renderline "" overtype::piper::renderline - -#intended primarily for single grapheme - but will work for multiple -#WARNING: query CAN contain ansi or newlines - but if cache was not already set manually,the answer will be incorrect! -#We deliberately allow this for PM/SOS attached within a column -#(a cache of ansifreestring_width calls - as these are quite regex heavy) -proc overtype::grapheme_width_cached {ch} { - variable grapheme_widths - if {[tcl::dict::exists $grapheme_widths $ch]} { - return [tcl::dict::get $grapheme_widths $ch] - } - set width [punk::char::ansifreestring_width $ch] - tcl::dict::set grapheme_widths $ch $width - return $width -} - - - -proc overtype::test_renderline {} { - set t \uFF5E ;#2-wide tilde - set u \uFF3F ;#2-wide underscore - set missing \uFFFD - return [list $t $u A${t}B] -} - -#maintenance warning -#same as textblock::size - but we don't want that circular dependency -#block width and height can be tricky. e.g \v handled differently on different terminal emulators and can affect both -proc overtype::blocksize {textblock} { - if {$textblock eq ""} { - return [tcl::dict::create width 0 height 1] ;#no such thing as zero-height block - for consistency with non-empty strings having no line-endings - } - if {[tcl::string::first \t $textblock] >= 0} { - if {[info exists punk::console::tabwidth]} { - set tw $::punk::console::tabwidth - } else { - set tw 8 - } - set textblock [textutil::tabify::untabify2 $textblock $tw] - } - #ansistrip on entire block in one go rather than line by line - result should be the same - review - make tests - if {[punk::ansi::ta::detect $textblock]} { - set textblock [punk::ansi::ansistrip $textblock] - } - if {[tcl::string::last \n $textblock] >= 0} { - set num_le [expr {[tcl::string::length $textblock]-[tcl::string::length [tcl::string::map {\n {}} $textblock]]}] ;#faster than splitting into single-char list - set width [tcl::mathfunc::max {*}[lmap v [split $textblock \n] {::punk::char::ansifreestring_width $v}]] - } else { - set num_le 0 - set width [punk::char::ansifreestring_width $textblock] - } - #our concept of block-height is likely to be different to other line-counting mechanisms - set height [expr {$num_le + 1}] ;# one line if no le - 2 if there is one trailing le even if no data follows le - - return [tcl::dict::create width $width height $height] ;#maintain order in 'image processing' standard width then height - caller may use lassign [dict values [blocksize ]] width height -} - -tcl::namespace::eval overtype::priv { - variable cache_is_sgr [tcl::dict::create] - - #we are likely to be asking the same question of the same ansi codes repeatedly - #caching the answer saves some regex expense - possibly a few uS to lookup vs under 1uS - #todo - test if still worthwhile after a large cache is built up. (limit cache size?) - proc is_sgr {code} { - variable cache_is_sgr - if {[tcl::dict::exists $cache_is_sgr $code]} { - return [tcl::dict::get $cache_is_sgr $code] - } - set answer [punk::ansi::codetype::is_sgr $code] - tcl::dict::set cache_is_sgr $code $answer - return $answer - } - # better named render_to_unapplied? - proc render_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list ;#maintaining as a list allows caller to utilize it without having to re-split - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - #append unapplied [join [lindex $overstacks $idx_over] ""] - #append unapplied [punk::ansi::codetype::sgr_merge_list {*}[lindex $overstacks $idx_over]] - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci+1 end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - - #clearer - renders the specific gci forward as unapplied - prefixed with it's merged sgr stack - proc render_this_unapplied {overlay_grapheme_control_list gci} { - upvar idx_over idx_over - upvar unapplied unapplied - upvar unapplied_list unapplied_list - upvar overstacks overstacks - upvar overstacks_gx overstacks_gx - upvar overlay_grapheme_control_stacks og_stacks - - #set unapplied [join [lrange $overlay_grapheme_control_list $gci+1 end]] - set unapplied "" - set unapplied_list [list] - - set sgr_merged [punk::ansi::codetype::sgr_merge_list {*}[lindex $og_stacks $gci]] - if {$sgr_merged ne ""} { - lappend unapplied_list $sgr_merged - } - switch -- [lindex $overstacks_gx $idx_over] { - "gx0_on" { - lappend unapplied_list "\x1b(0" - } - "gx0_off" { - lappend unapplied_list "\x1b(B" - } - } - - foreach gc [lrange $overlay_grapheme_control_list $gci end] { - lassign $gc type item - #types g other sgr gx0 - if {$type eq "gx0"} { - if {$item eq "gx0_on"} { - lappend unapplied_list "\x1b(0" - } elseif {$item eq "gx0_off"} { - lappend unapplied_list "\x1b(B" - } - } else { - lappend unapplied_list $item - } - } - set unapplied [join $unapplied_list ""] - } - proc render_delchar {i} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - set nxt [llength $o] - if {$i < $nxt} { - set o [lreplace $o $i $i] - set ustacks [lreplace $ustacks $i $i] - set gxstacks [lreplace $gxstacks $i $i] - } elseif {$i == 0 || $i == $nxt} { - #nothing to do - } else { - puts stderr "render_delchar - attempt to delchar at index $i >= number of outcols $nxt - shouldn't happen" - } - } - proc render_erasechar {i count} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - upvar replay_codes_overlay replay - #ECH clears character attributes from erased character positions - #ECH accepts 0 or empty parameter, which is equivalent to 1. Caller of render_erasechar should do that mapping and only supply 1 or greater. - if {![tcl::string::is integer -strict $count] || $count < 1} { - error "render_erasechar count must be integer >= 1" - } - set start $i - set end [expr {$i + $count -1}] - #we restrict ECH to current line - as some terminals do - review - is that the only way it's implemented? - if {$i > [llength $o]-1} { - return - } - if {$end > [llength $o]-1} { - set end [expr {[llength $o]-1}] - } - set num [expr {$end - $start + 1}] - set o [lreplace $o $start $end {*}[lrepeat $num \u0000]] ;#or space? - #DECECM ??? - set ustacks [lreplace $ustacks $start $end {*}[lrepeat $num [list $replay]]] - set gxstacks [lreplace $gxstacks $start $end {*}[lrepeat $num [list]]] ;# ??? review - return - } - proc render_setchar {i c } { - upvar outcols o - lset o $i $c - } - - #Initial usecase is for old-terminal hack to add PM-wrapped \b - #review - can be used for other multibyte sequences that occupy one column? - #combiners? diacritics? - proc render_append_to_char {i c} { - upvar outcols o - if {$i > [llength $o]-1} { - error "render_append_to_char cannot append [ansistring VIEW -lf 1 -nul 1 $c] to existing char at index $i while $i >= llength outcols [llength $o]" - } - set existing [lindex $o $i] - if {$existing eq "\0"} { - lset o $i $c - } else { - lset o $i $existing$c - } - } - #is actually addgrapheme? - proc render_addchar {i c sgrstack gx0stack {insert_mode 0}} { - upvar outcols o - upvar understacks ustacks - upvar understacks_gx gxstacks - - # -- --- --- - #this is somewhat of a hack.. probably not really the equivalent of proper reverse video? review - #we should ideally be able to reverse the video of a sequence that already includes SGR reverse/noreverse attributes - upvar reverse_mode do_reverse - #if {$do_reverse} { - # lappend sgrstack [a+ reverse] - #} else { - # lappend sgrstack [a+ noreverse] - #} - - #JMN3 - if {$do_reverse} { - #note we can't just look for \x1b\[7m or \x1b\[27m - # it may be a more complex sequence like \x1b\[0\;\;7\;31m etc - - set existing_reverse_state 0 - set codeinfo [punk::ansi::codetype::sgr_merge $sgrstack -info 1] - set codestate_reverse [dict get $codeinfo codestate reverse] - switch -- $codestate_reverse { - 7 { - set existing_reverse_state 1 - } - 27 { - set existing_reverse_state 0 - } - "" { - } - } - if {$existing_reverse_state == 0} { - set rflip [a+ reverse] - } else { - #reverse of reverse - set rflip [a+ noreverse] - } - #note that mergeresult can have multiple esc (due to unmergeables or non sgr codes) - set sgrstack [list [dict get $codeinfo mergeresult] $rflip] - #set sgrstack [punk::ansi::codetype::sgr_merge [list [dict get $codeinfo mergeresult] $rflip]] - } - - # -- --- --- - - set nxt [llength $o] - if {!$insert_mode} { - if {$i < $nxt} { - #These lists must always be in sync - lset o $i $c - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - lset ustacks $i $sgrstack - lset gxstacks $i $gx0stack - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } else { - #insert of single-width vs double-width when underlying is double-width? - if {$i < $nxt} { - set o [linsert $o $i $c] - } else { - lappend o $c - } - if {$i < [llength $ustacks]} { - set ustacks [linsert $ustacks $i $sgrstack] - set gxstacks [linsert $gxstacks $i $gx0stack] - } else { - lappend ustacks $sgrstack - lappend gxstacks $gx0stack - } - } - } - -} - - - -# -- --- --- --- --- --- --- --- --- --- --- -tcl::namespace::eval overtype { - interp alias {} ::overtype::center {} ::overtype::centre -} - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -package provide overtype [tcl::namespace::eval overtype { - variable version - set version 1.6.5 -}] -return - -#*** !doctools -#[manpage_end] diff --git a/src/bootsupport/modules/pattern-1.2.4.tm b/src/bootsupport/modules/pattern-1.2.4.tm index 5d76af04..d6a9c932 100644 --- a/src/bootsupport/modules/pattern-1.2.4.tm +++ b/src/bootsupport/modules/pattern-1.2.4.tm @@ -1,1285 +1,1285 @@ -#PATTERN -# - A prototype-based Object system. -# -# Julian Noble 2003 -# License: Public domain -# - -# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. -# -# -# Pattern uses a mixture of class-based and prototype-based object instantiation. -# -# A pattern object has 'properties' and 'methods' -# The system makes a distinction between them with regards to the access syntax for write operations, -# and yet provides unity in access syntax for read operations. -# e.g >object . myProperty -# will return the value of the property 'myProperty' -# >ojbect . myMethod -# will return the result of the method 'myMethod' -# contrast this with the write operations: -# set [>object . myProperty .] blah -# >object . myMethod blah -# however, the property can also be read using: -# set [>object . myProperty .] -# Note the trailing . to give us a sort of 'reference' to the property. -# this is NOT equivalent to -# set [>object . myProperty] -# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property -# i.e it is equivalent in this case to: set blah - -#All objects are represented by a command, the name of which contains a leading ">". -#Any commands in the interp which use this naming convention are assumed to be a pattern object. -#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) - -#All user-added properties & methods of the wrapped object are accessed -# using the separator character "." -#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." -# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) -# you would use the 'Create' metamethod on the pattern object like so: -# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject -# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties -# of the object it was created from. ( - - -#The use of the access-syntax separator character "." allows objects to be kept -# 'clean' in the sense that the only methods &/or properties that can be called this way are ones -# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax -# so you are free to implement your own 'Create' method on your object that doesn't conflict with -# the metamethod. - -#Chainability (or how to violate the Law of Demeter!) -#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other -# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference -# structure, without the need to regress to enter matching brackets as is required when using -# standard TCL command syntax. -# ie instead of: -# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething -# we can use: -# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething -# -# This separates out the object-traversal syntax from the TCL command syntax. - -# . is the 'traversal operator' when it appears between items in a commandlist -# . is the 'reference operator' when it is the last item in a commandlist -# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. -# It marks breaks in the multidimensional structure that correspond to how the data is stored. -# e.g obj . arraydata x y , x1 y1 z1 -# represents an element of a 5-dimensional array structured as a plane of cubes -# e.g2 obj . arraydata x y z , x1 y1 -# represents an element of a 5-dimensional array structured as a cube of planes -# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 -# .. is the 'meta-traversal operator' when it appears between items in a commandlist -# .. is the 'meta-info operator'(?) when it is the last item in a commandlist - - -#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing -# implement iStacks & pStacks (interface stacks & pattern stacks) - -#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 - - -#------------------------------------------------------------ -# System objects. -#------------------------------------------------------------ -#::p::-1 ::p::internals::>metaface -#::p::0 ::p::ifaces::>null -#::p::1 ::>pattern -#------------------------------------------------------------ - -#TODO - -#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) - - -#CHANGES -#2018-09 - v 1.2.2 -# varied refactoring -# Changed invocant datastructure curried into commands (the _ID_ structure) -# Changed MAP structure to dict -# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) -# updated test suites -#2018-08 - v 1.2.1 -# split ::p::predatorX functions into separate files (pkgs) -# e.g patternpredator2-1.0.tm -# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken -# -#2017-08 - v 1.1.6 Fairly big overhaul -# New predator function using coroutines -# Added bang operator ! -# Fixed Constructor chaining -# Added a few tests to test::pattern -# -#2008-03 - preserve ::errorInfo during var writes - -#2007-11 -#Major overhaul + new functionality + new tests v 1.1 -# new dispatch system - 'predator'. -# (preparing for multiple interface stacks, multiple invocants etc) -# -# -#2006-05 -# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. -# -#2005-12 -# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. -# -# Fixed so that PatternVariable default applied on Create. -# -# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: -# - heading towards multiple-interface objects -# -#2005-10-28 -# 1.0.8.1 passes 80/80 tests -# >object .. Destroy - improved cleanup of interfaces & namespaces. -# -#2005-10-26 -# fixes to refsync (still messy!) -# remove variable traces on REF vars during .. Destroy -# passes 76/76 -# -#2005-10-24 -# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. -# 1.0.8.0 now passes 75/76 -# -#2005-10-19 -# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) -# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) -# 1.0.8.0 (passes 74/76) -# tests now in own package -# usage: -# package require test::pattern -# test::p::list -# test::p::run ?nameglob? ?-version ? -# -#2005-09?-12 -# -# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. -# fixed @next@ so that destination method resolved at interface compile time instead of call time -# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. -# (before, the overlay only occured when '.. Method' was used to override.) -# -# -# miscellaneous tidy-ups -# -# 1.0.7.8 (passes 71/73) -# -#2005-09-10 -# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value -# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. -# -#2005-09-07 -# bugfix indexed write to list property -# bugfix Variable default value -# 1.0.7.7 (passes 70/72) -# fails: -# arrayproperty.test - array-entire-reference -# properties.test - property_getter_filter_via_ObjectRef -# -#2005-04-22 -# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) -# -# 1.0.7.4 -# -#2004-11-05 -# basic PropertyRead implementation (non-indexed - no tests!) -# -#2004-08-22 -# object creation speedups - (pattern::internals::obj simplified/indirected) -# -#2004-08-17 -# indexed property setter fixes + tests -# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) -# -#2004-08-16 -# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) -# -#2004-08-15 -# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) -# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger -# - also trigger on curried traces to indexed properties i.e list and array elements. -# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. -# -# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] -# -#2004-08-05 -# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) -# -# fix + add tests to support method & property of same name. (method precedence) -# -#2004-08-04 -# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) -# -# 1.0.7.1 -# use objectref array access to read properties even when some props unset; + test -# unset property using array access on object reference; + test -# -# -#2004-07-21 -# object reference changes - array property values appear as list value when accessed using upvared array. -# bugfixes + tests - properties containing lists (multidimensional access) -# -#1.0.7 -# -#2004-07-20 -# fix default property value append problem -# -#2004-07-17 -# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods -# ( -# -#2004-06-18 -# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. -# -#2004-06-05 -# change argsafety operator to be anything with leading - -# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' -# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, -# the entire dash-prefixed operator is also passed in as an argument. -# e.g >object . doStuff -window . -# will call the doStuff method with the 2 parameters -window . -# >object . doStuff - . -# will call doStuff with single parameter . -# >object . doStuff - -window . -# will result in a reference to the doStuff method with the argument -window 'curried' in. -# -#2004-05-19 -#1.0.6 -# fix so custom constructor code called. -# update Destroy metamethod to unset $self -# -#1.0.4 - 2004-04-22 -# bug fixes regarding method specialisation - added test -# -#------------------------------------------------------------ - -package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] - - -namespace eval pattern::util { - - # Generally better to use 'package require $minver-' - # - this only gives us a different error - proc package_require_min {pkg minver} { - if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { - package require $pkg - } else { - error "Package pattern requires package $pkg of at least version $minver. Available: $available" - } - } -} - -package require patterncmd 1.2.4- -package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) - - - -#package require cmdline -package require overtype - -#package require md5 ;#will be loaded if/when needed -#package require md4 -#package require uuid - - - - - -namespace eval pattern { - variable initialised 0 - - - if 0 { - if {![catch {package require twapi_base} ]} { - #twapi is a windows only package - #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. - # If available - windows seems to provide a fast uuid generator.. - #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) - # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) - interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok - } else { - #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) - # (e.g 200usec 2018 corei9) - #(with or without tcllibc?) - #very first call is extremely slow though - 3.5seconds on 2018 corei9 - package require uuid - interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate - } - #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) - } - - -} - - - - - - -namespace eval p { - #this is also the interp alias namespace. (object commands created here , then renamed into place) - #the object aliases are named as incrementing integers.. !todo - consider uuids? - variable ID 0 - namespace eval internals {} - - - #!?? - #namespace export ?? - variable coroutine_instance 0 -} - -#------------------------------------------------------------------------------------- -#review - what are these for? -#note - this function is deliberately not namespaced -# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features -proc process_pattern_aliases {object args} { - set o [namespace tail $object] - interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] - interp alias {} process_method_$o {} [$object .. Method .] - interp alias {} process_constructor_$o {} [$object .. Constructor .] -} -#------------------------------------------------------------------------------------- - - - - -#!store all interface objects here? -namespace eval ::p::ifaces {} - - - -#K combinator - see http://wiki.tcl.tk/1923 -#proc ::p::K {x y} {set x} -#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] - - - - - - - - -proc ::p::internals::(VIOLATE) {_ID_ violation_script} { - #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] - set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] - - if {![dict get $processed explicitvars]} { - #no explicit var statements - we need the implicit ones - set self [set ::p::${_ID_}::(self)] - set IFID [lindex [set $self] 1 0 end] - #upvar ::p::${IFID}:: self_IFINFO - - - set varDecls {} - set vlist [array get ::p::${IFID}:: v,name,*] - set _k ""; set v "" - if {[llength $vlist]} { - append varDecls "upvar #0 " - foreach {_k v} $vlist { - append varDecls "::p::\${_ID_}::$v $v " - } - append varDecls "\n" - } - - #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] - set violation_script $varDecls\n[dict get $processed body] - - #tidy up - unset processed varDecls self IFID _k v - } else { - set violation_script [dict get $processed body] - } - unset processed - - - - - #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. - eval "unset violation_script;$violation_script" -} - - -proc ::p::internals::DestroyObjectsBelowNamespace {ns} { - #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" - - set nsparts [split [string trim [string map {:: :} $ns] :] :] - if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { - #ns not of form ::p::?::_ref - - foreach obj [info commands ${ns}::>*] { - #catch {::p::meta::Destroy $obj} - #puts ">>found object $obj below ns $ns - destroying $obj" - $obj .. Destroy - } - } - - #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] - #foreach tinfo $traces { - # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo - #} - #unset -nocomplain ${ns}::-->PATTERN_ANCHOR - - foreach sub [namespace children $ns] { - ::p::internals::DestroyObjectsBelowNamespace $sub - } -} - - - - -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# - - - - - - - - - -proc ::p::get_new_object_id {} { - tailcall incr ::p::ID - #tailcall ::pattern::new_uuid -} - -#create a new minimal object - with no interfaces or patterns. - -#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} -proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { - - #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" - - if {$OID eq "-2"} { - set OID [::p::get_new_object_id] - #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) - #set OID [pattern::new_uuid] - } - #if $wrapped provided it is assumed to be an existing namespace. - #if {[string length $wrapped]} { - # #??? - #} - - #sanity check - alias must not exist for this OID - if {[llength [interp alias {} ::p::$OID]]} { - error "Object alias '::p::$OID' already exists - cannot create new object with this id" - } - - #system 'varspaces' - - - #until we have a version of Tcl that doesn't have 'creative writing' scope issues - - # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. - # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') - #set o_open 1 - every object is initially also an open interface (?) - #NOTE! comments within namespace eval slow it down. - namespace eval ::p::$OID { - #namespace ensemble create - namespace eval _ref {} - namespace eval _meta {} - namespace eval _iface { - variable o_usedby; - variable o_open 1; - array set o_usedby [list]; - variable o_varspace "" ; - variable o_varspaces [list]; - variable o_methods [dict create]; - variable o_properties [dict create]; - variable o_variables; - variable o_propertyunset_handlers; - set o_propertyunset_handlers [dict create] - } - } - - #set alias ::p::$OID - - #objectid alis default_method object_command wrapped_namespace - set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] - - #MAP is a dict - set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] - - - - #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token - #we've already checked that ::p::$OID doesn't pre-exist - # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias - #interp alias {} ::p::$OID {} ::p::internals::predator $MAP - - - # _ID_ structure - set invocants_dict [dict create this [list $INVOCANTDATA] ] - #puts stdout "New _ID_structure: $interfaces_dict" - set _ID_ [dict create i $invocants_dict context ""] - - - interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ - #rename the command into place - thus the alias & the command name no longer match! - rename ::p::$OID $cmd - - set ::p::${OID}::_meta::map $MAP - - # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something - interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ - - #set p2 [string map {> ?} $cmd] - #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ - - - #trace add command $cmd delete "$cmd .. Destroy ;#" - #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" - - trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" - #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) - - #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" - - - #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" - #trace add command $cmd delete "puts deleting$cmd ;#" - #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" - - - #puts "--> new_object returning map $MAP" - return $MAP -} - - - - -#>x .. Create >y -# ".." is special case equivalent to "._." -# (whereas in theory it would be ".default.") -# "." is equivalent to ".default." is equivalent to ".default.default." (...) - -#>x ._. Create >y -#>x ._.default. Create >y ??? -# -# - -# create object using 'blah' as source interface-stack ? -#>x .blah. .. Create >y -#>x .blah,_. ._. Create .iStackDestination. >y - - - -# -# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] -# the 1st item, blah in this case becomes the 'default' iStack. -# -#>x .*. -# cast to object with all iStacks -# -#>x .*,!_. -# cast to object with all iStacks except _ -# -# --------------------- -#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' -# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. -# -#eg1: >x & >y . some_multi_method arg arg -# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) -# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' -# The invocant signature is thus {these 2} -# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) -# Invocation roles can be specified in the call using the @ operator. -# e.g >x & >y @ points . some_multi_method arg arg -# The invocant signature for this is: {points 2} -# -#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path -# This has the signature {objects n plane 1} where n depends on the length of the list $objects -# -# -# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. -# e.g set pointset [>x & >y .] -# We can now call multimethods on $pointset -# - - - - - - -#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) -proc ::pattern::predatorversion {{ver ""}} { - variable active_predatorversion - set allowed_predatorversions {1 2} - set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions - - if {![info exists active_predatorversion]} { - set first_time_set 1 - } else { - set first_time_set 0 - } - - if {$ver eq ""} { - #get version - if {$first_time_set} { - set active_predatorversions $default_predatorversion - } - return $active_predatorversion - } else { - #set version - if {$ver ni $allowed_predatorversions} { - error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" - } - - if {!$first_time_set} { - if {$active_predatorversion eq $ver} { - #puts stderr "Active predator version is already '$ver'" - #ok - nothing to do - return $active_predatorversion - } else { - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - rename ::p::internals::predator ::p::predator$active_predatorversion - } - } - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - - rename ::p::predator$ver ::p::internals::predator - set active_predatorversion $ver - - return $active_predatorversion - } -} -::pattern::predatorversion 2 - - - - - - - - - - - - -# >pattern has object ID 1 -# meta interface has object ID 0 -proc ::pattern::init args { - - if {[set ::pattern::initialised]} { - if {[llength $args]} { - #if callers want to avoid this error, they can do their own check of $::pattern::initialised - error "pattern package is already initialised. Unable to apply args: $args" - } else { - return 1 - } - } - - #this seems out of date. - # - where is PatternPropertyRead? - # - Object is obsolete - # - Coinjoin, Combine don't seem to exist - array set ::p::metaMethods { - Clone object - Conjoin object - Combine object - Create object - Destroy simple - Info simple - Object simple - PatternProperty simple - PatternPropertyWrite simple - PatternPropertyUnset simple - Property simple - PropertyWrite simple - PatternMethod simple - Method simple - PatternVariable simple - Variable simple - Digest simple - PatternUnknown simple - Unknown simple - } - array set ::p::metaProperties { - Properties object - Methods object - PatternProperties object - PatternMethods object - } - - - - - - #create metaface - IID = -1 - also OID = -1 - # all objects implement this special interface - accessed via the .. operator. - - - - - - set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface - - - #OID = 0 - ::p::internals::new_object ::p::ifaces::>null "" 0 - - #? null object has itself as level0 & level1 interfaces? - #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] - - #null interface should always have 'usedby' members. It should never be extended. - array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array - set ::p::0::_iface::o_open 0 - - set ::p::0::_iface::o_constructor [list] - set ::p::0::_iface::o_variables [list] - set ::p::0::_iface::o_properties [dict create] - set ::p::0::_iface::o_methods [dict create] - set ::p::0::_iface::o_varspace "" - set ::p::0::_iface::o_varspaces [list] - array set ::p::0::_iface::o_definition [list] - set ::p::0::_iface::o_propertyunset_handlers [dict create] - - - - - ############################### - # OID = 1 - # >pattern - ############################### - ::p::internals::new_object ::>pattern "" 1 - - #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] - - - array set ::p::1::_iface::o_usedby [list] ;#'usedby' array - - set _self ::pattern - - #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 - #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 - - - - #1)this object references its interfaces - #lappend ID $IFID $IFID_1 - #lset SELFMAP 1 0 $IFID - #lset SELFMAP 2 0 $IFID_1 - - - #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] - #proc ::>pattern args $body - - - - - ####################################################################################### - #OID = 2 - # >ifinfo interface for accessing interfaces. - # - ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object - set ::p::2::_iface::o_constructor [list] - set ::p::2::_iface::o_variables [list] - set ::p::2::_iface::o_properties [dict create] - set ::p::2::_iface::o_methods [dict create] - set ::p::2::_iface::o_varspace "" - set ::p::2::_iface::o_varspaces [list] - array set ::p::2::_iface::o_definition [list] - set ::p::2::_iface::o_open 1 ;#open for extending - - ::p::ifaces::>2 .. AddInterface 2 - - #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations - #(bootstrap because we can't yet use metaface methods on it) - - - - proc ::p::2::_iface::isOpen.1 {_ID_} { - return $::p::2::_iface::o_open - } - interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 - - proc ::p::2::_iface::isClosed.1 {_ID_} { - return [expr {!$::p::2::_iface::o_open}] - } - interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 - - proc ::p::2::_iface::open.1 {_ID_} { - set ::p::2::_iface::o_open 1 - } - interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 - - proc ::p::2::_iface::close.1 {_ID_} { - set ::p::2::_iface::o_open 0 - } - interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 - - - #proc ::p::2::_iface::(GET)properties.1 {_ID_} { - # set ::p::2::_iface::o_properties - #} - #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 - - #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties - - - #proc ::p::2::_iface::(GET)methods.1 {_ID_} { - # set ::p::2::_iface::o_methods - #} - #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 - #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods - - - - - - #link from object to interface (which in this case are one and the same) - - #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] - #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] - #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] - #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] - - interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen - interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed - interp alias {} ::p::2::open {} ::p::2::_iface::open - interp alias {} ::p::2::close {} ::p::2::_iface::close - - - #namespace eval ::p::2 "namespace export $method" - - ####################################################################################### - - - - - - - set ::pattern::initialised 1 - - - ::p::internals::new_object ::p::>interface "" 3 - #create a convenience object on which to manipulate the >ifinfo interface - #set IF [::>pattern .. Create ::p::>interface] - set IF ::p::>interface - - - #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? - # (or is forcing end user to add their own pStack/iStack ok .. ?) - # - ::p::>interface .. AddPatternInterface 2 ;# - - ::p::>interface .. PatternVarspace _iface - - ::p::>interface .. PatternProperty methods - ::p::>interface .. PatternPropertyRead methods {} { - varspace _iface - var {o_methods alias} - return $alias - } - ::p::>interface .. PatternProperty properties - ::p::>interface .. PatternPropertyRead properties {} { - varspace _iface - var o_properties - return $o_properties - } - ::p::>interface .. PatternProperty variables - - ::p::>interface .. PatternProperty varspaces - - ::p::>interface .. PatternProperty definition - - ::p::>interface .. Constructor {{usedbylist {}}} { - #var this - #set this @this@ - #set ns [$this .. Namespace] - #puts "-> creating ns ${ns}::_iface" - #namespace eval ${ns}::_iface {} - - varspace _iface - var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces - - set o_constructor [list] - set o_variables [list] - set o_properties [dict create] - set o_methods [dict create] - set o_varspaces [list] - array set o_definition [list] - - foreach usedby $usedbylist { - set o_usedby(i$usedby) 1 - } - - - } - ::p::>interface .. PatternMethod isOpen {} { - varspace _iface - var o_open - - return $o_open - } - ::p::>interface .. PatternMethod isClosed {} { - varspace _iface - var o_open - - return [expr {!$o_open}] - } - ::p::>interface .. PatternMethod open {} { - varspace _iface - var o_open - set o_open 1 - } - ::p::>interface .. PatternMethod close {} { - varspace _iface - var o_open - set o_open 0 - } - ::p::>interface .. PatternMethod refCount {} { - varspace _iface - var o_usedby - return [array size o_usedby] - } - - set ::p::2::_iface::o_open 1 - - - - - uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} - #uplevel #0 {package require patternlib} - return 1 -} - - - -proc ::p::merge_interface {old new} { - #puts stderr " ** ** ** merge_interface $old $new" - set ns_old ::p::$old - set ns_new ::p::$new - - upvar #0 ::p::${new}:: IFACE - upvar #0 ::p::${old}:: IFACEX - - if {![catch {set c_arglist $IFACEX(c,args)}]} { - #constructor - #for now.. just add newer constructor regardless of any existing one - #set IFACE(c,args) $IFACEX(c,args) - - #if {![info exists IFACE(c,args)]} { - # #target interface didn't have a constructor - # - #} else { - # # - #} - } - - - set methods [::list] - foreach nm [array names IFACEX m-1,name,*] { - lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) - } - - #puts " *** merge interface $old -> $new ****merging-in methods: $methods " - - foreach method $methods { - if {![info exists IFACE(m-1,name,$method)]} { - #target interface doesn't yet have this method - - set THISNAME $method - - if {![string length [info command ${ns_new}::$method]]} { - - if {![set ::p::${old}::_iface::o_open]} { - #interp alias {} ${ns_new}::$method {} ${ns_old}::$method - #namespace eval $ns_new "namespace export [namespace tail $method]" - } else { - #wait to compile - } - - } else { - error "merge interface - command collision " - } - #set i 2 ??? - set i 1 - - } else { - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - - set i [incr IFACE(m-1,chain,$method)] - - set THISNAME ___system___override_${method}_$i - - #move metadata using subindices for delegated methods - set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) - set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) - set IFACE(mp-$i,$method) $IFACE(mp-1,$method) - - set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) - set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) - - - #set next [::p::next_script $IFID0 $method] - if {![string length [info command ${ns_new}::$THISNAME]]} { - if {![set ::p::${old}::_iface::o_open]} { - interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method - namespace eval $ns_new "namespace export $method" - } else { - #wait for compile - } - } else { - error "merge_interface - command collision " - } - - } - - array set IFACE [::list \ - m-1,chain,$method $i \ - m-1,body,$method $IFACEX(m-1,body,$method) \ - m-1,args,$method $IFACEX(m-1,args,$method) \ - m-1,name,$method $THISNAME \ - m-1,iface,$method $old \ - ] - - } - - - - - - #array set ${ns_new}:: [array get ${ns_old}::] - - - #!todo - review - #copy everything else across.. - - foreach {nm v} [array get IFACEX] { - #puts "-.- $nm" - if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { - set IFACE($nm) $v - } - } - - #!todo -write a test - set ::p::${new}::_iface::o_open 1 - - #!todo - is this done also when iface compiled? - #namespace eval ::p::$new {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place - - return -} - - - - -#detect attempt to treat a reference to a method as a property -proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { -#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" - lassign [lrange $args end-2 end] vtraced vidx op - #NOTE! cannot rely on vtraced as it may have been upvared - - switch -- $op { - write { - error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - unset { - #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace - #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #!todo - don't use vtraced! - trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #pointless raising an error as "Any errors in unset traces are ignored" - #error "cannot unset. $field is a method not a property" - } - read { - error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - array { - error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" - #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" - } - } - - return -} - - - - -#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. -# -# The 'dispatcher' is an object instance's underlying object command. -# - -#proc ::p::make_dispatcher {obj ID IFID} { -# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { -# ::p::@IID@ $methprop @oid@ {*}$args -# }] -# return -#} - - - - -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -#aliased from ::p::${OID}:: -# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something -proc ::p::internals::no_default_method {_ID_ args} { - puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" - lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped - tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" -} - -#force 1 will extend an interface even if shared. (??? why is this necessary here?) -#if IID empty string - create the interface. -proc ::p::internals::expand_interface {IID {force 0}} { - #puts stdout ">>> expand_interface $IID [info level -1]<<<" - if {![string length $IID]} { - #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) - set iid [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$iid - return $iid - } else { - if {[set ::p::${IID}::_iface::o_open]} { - #interface open for extending - shared or not! - return $IID - } - - if {[array size ::p::${IID}::_iface::o_usedby] > 1} { - #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby - - #oops.. shared interface. Copy before specialising it. - set prev_IID $IID - - #set IID [::p::internals::new_interface] - set IID [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$IID - - ::p::internals::linkcopy_interface $prev_IID $IID - #assert: prev_usedby contains at least one other element. - } - - #whether copied or not - mark as open for extending. - set ::p::${IID}::_iface::o_open 1 - return $IID - } -} - -#params: old - old (shared) interface ID -# new - new interface ID -proc ::p::internals::linkcopy_interface {old new} { - #puts stderr " ** ** ** linkcopy_interface $old $new" - set ns_old ::p::${old}::_iface - set ns_new ::p::${new}::_iface - - - - foreach nsmethod [info commands ${ns_old}::*.1] { - #puts ">>> adding $nsmethod to iface $new" - set tail [namespace tail $nsmethod] - set method [string range $tail 0 end-2] ;#strip .1 - - if {![llength [info commands ${ns_new}::$method]]} { - - set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 - - #link from new interface namespace to existing one. - #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) - #!todo? verify? - #- actual link is chainslot to chainslot - interp alias {} ${ns_new}::$method.1 {} $oldhead - - #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? - - - #chainhead pointer within new interface - interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 - - namespace eval $ns_new "namespace export $method" - - #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { - # lappend ${ns_new}::o_methods $method - #} - } else { - if {$method eq "(VIOLATE)"} { - #ignore for now - #!todo - continue - } - - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - #warning - existing chainslot will be completely shadowed by linked method. - # - existing one becomes unreachable. #!todo review!? - - - error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" - - } - } - - - #foreach propinf [set ${ns_old}::o_properties] { - # lassign $propinf prop _default - # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop - # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop - # lappend ${ns_new}::o_properties $propinf - #} - - - set ${ns_new}::o_variables [set ${ns_old}::o_variables] - set ${ns_new}::o_properties [set ${ns_old}::o_properties] - set ${ns_new}::o_methods [set ${ns_old}::o_methods] - set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] - - - set ::p::${old}::_iface::o_usedby(i$new) linkcopy - - - #obsolete.? - array set ::p::${new}:: [array get ::p::${old}:: ] - - - - #!todo - is this done also when iface compiled? - #namespace eval ::p::${new}::_iface {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' - - return -} -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -pattern::init - -return $::pattern::version +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #until we have a version of Tcl that doesn't have 'creative writing' scope issues - + # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. + # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/bootsupport/modules/punk-0.1.tm b/src/bootsupport/modules/punk-0.1.tm index 68a14411..6fb185a9 100644 --- a/src/bootsupport/modules/punk-0.1.tm +++ b/src/bootsupport/modules/punk-0.1.tm @@ -1,4 +1,4 @@ -#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. +#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. #Built on Tcl of course - because it's the most powerful piece of under-appreciated and alternate-thinking engineering you can plug into. @@ -6,8 +6,8 @@ namespace eval punk { proc lazyload {pkg} { package require zzzload if {[package provide $pkg] eq ""} { - zzzload::pkg_require $pkg - } + zzzload::pkg_require $pkg + } } #lazyload twapi ? @@ -50,9 +50,9 @@ namespace eval punk { } - proc ::punk::auto_execok_original name [info body ::auto_execok] + proc ::punk::auto_execok_original name [info body ::auto_execok] variable better_autoexec - + #set better_autoexec 0 ;#use this var via better_autoexec only #proc ::punk::auto_execok_windows name { # ::punk::auto_execok_original $name @@ -141,6 +141,7 @@ namespace eval punk { } if {[llength [file split $name]] != 1} { + #has a path foreach ext $execExtensions { set file ${name}${ext} if {[file exists $file] && ![file isdirectory $file]} { @@ -164,21 +165,45 @@ namespace eval punk { } foreach var {PATH Path path} { - if {[info exists env($var)]} { - append path ";$env($var)" - } + if {[info exists env($var)]} { + append path ";$env($var)" + } } #change2 - set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { + set lookfor [list $name] + } else { + set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + } + #puts "-->$lookfor" foreach dir [split $path {;}] { + set dir [string trim $dir {\\}] ;#trailing slash will result in a tail such as "/python.exe" #set dir [file normalize $dir] # Skip already checked directories if {[info exists checked($dir)] || ($dir eq "")} { continue } set checked($dir) {} - + + #surprisingly fast + #set matches [glob -nocomplain -dir $dir -types f -tails {*}$lookfor] + ##puts "--dir $dir matches:$matches" + #if {[llength $matches]} { + # set file [file join $dir [lindex $matches 0]] + # #puts "--match0:[lindex $matches 0] file:$file" + # return [set auto_execs($name) [list $file]] + #} + + #what if it's a link? + #foreach match [glob -nocomplain -dir $dir -types f -tail {*}$lookfor] { + # set file [file join $dir $match] + # if {[file exists $file]} { + # return [set auto_execs($name) [list $file]] + # } + #} + + #safest? could be a link? foreach match [glob -nocomplain -dir $dir -tail {*}$lookfor] { set file [file join $dir $match] if {[file exists $file] && ![file isdirectory $file]} { @@ -209,7 +234,7 @@ namespace eval punk { #review - what if punk package reloaded - but ::auto_execs has updated path for winget.exe? #what if we create another interp and use the same ::auto_execs? The appdir won't be detected. #TODO - see if there is a proper windows way to determine where the 'reparse point' apps are installed - + #winget is installed on all modern windows and is an example of the problem this addresses @@ -223,9 +248,9 @@ namespace eval punk { upvar ::punk::can_exec_windowsapp can_exec_windowsapp upvar ::punk::windowsappdir windowsappdir upvar ::punk::cmdexedir cmdexedir - + if {$windowsappdir eq ""} { - #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' + #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' #Tcl (2025) can't exec when given a path to these 0KB files #This path is probably something like C:/Users/username/AppData/Local/Microsoft/WindowsApps if {!([info exists ::env(LOCALAPPDATA)] && @@ -261,13 +286,13 @@ namespace eval punk { return [file join $windowsappdir $name] } if {$cmdexedir eq ""} { - #cmd.exe very unlikely to move + #cmd.exe very unlikely to move set cmdexedir [file dirname [lindex [::punk::auto_execok_windows cmd.exe] 0]] #auto_reset never seems to exist as a command - because auto_reset deletes all commands in the ::auto_index - #anyway.. it has other side effects (affects auto_load) + #anyway.. it has other side effects (affects auto_load) } return "[file join $cmdexedir cmd.exe] /c $name" - } + } return $default_auto }] @@ -279,9 +304,9 @@ namespace eval punk { #repltelemetry cooperation with other packages such as shellrun -#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists +#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists namespace eval punk { - variable repltelemetry_emmitters + variable repltelemetry_emmitters #don't stomp.. even if something created this namespace in advance and is 'cooperating' a bit early if {![info exists repltelemetry_emitters]} { set repltelemetry_emmitters [list] @@ -376,7 +401,7 @@ if {![llength [info commands ::ansistring]]} { package require punk::aliascore ;#mostly punk::lib aliases punk::aliascore::init -force 1 -package require punk::repl::codethread +package require punk::repl::codethread package require punk::config #package require textblock package require punk::console ;#requires Thread @@ -385,6 +410,9 @@ package require punk::winpath ;# for windows paths - but has functions that can package require punk::repo package require punk::du package require punk::mix::base +package require base64 + +package require punk::pipe namespace eval punk { # -- --- --- @@ -415,7 +443,7 @@ namespace eval punk { package require shellfilter package require punkapp package require funcl - + package require struct::list package require fileutil #package require punk::lib @@ -435,8 +463,8 @@ namespace eval punk { #----------------------------------- # todo - load initial debug state from config debug off punk.unknown - debug level punk.unknown 1 - debug off punk.pipe + debug level punk.unknown 1 + debug off punk.pipe debug level punk.pipe 4 debug off punk.pipe.var debug level punk.pipe.var 4 @@ -478,7 +506,7 @@ namespace eval punk { uplevel 1 [list set $varname $obj2] } - interp alias "" strlen "" ::punk::strlen + interp alias "" strlen "" ::punk::strlen interp alias "" str_len "" ::punk::strlen interp alias "" objclone "" ::punk::objclone #proc ::strlen {str} { @@ -487,6 +515,7 @@ namespace eval punk { #proc ::objclone {obj} { # append obj2 $obj {} #} + #----------------------------------------------------------------------------------- #order of arguments designed for pipelining #review - 'piper_' prefix is a naming convention for functions that are ordered for tail-argument pipelining @@ -502,6 +531,351 @@ namespace eval punk { proc ::punk::K {x y} { return $x} + #todo ansigrep? e.g grep using ansistripped value + proc grepstr1 {pattern data} { + set data [string map {\r\n \n} $data] + set lines [split $data \n] + set matches [lsearch -all -regexp $lines $pattern] + set max [lindex $matches end] + set w1 [string length $max] + set result "" + set H [a+ green bold overline] + set R \x1b\[m + foreach m $matches { + set ln [lindex $lines $m] + set ln [regsub -all $pattern $ln $H&$R] + append result [format %${w1}s $m] " $ln" \n + } + set result [string trimright $result \n] + return $result + } + + #---------------------- + #todo - fix overtype + #create test + #overtype::renderline -insert_mode 0 -transparent 1 [a+ green]-----[a] " [a+ underline]x[a]" + #---------------------- + + + punk::args::define { + @id -id ::punk::grepstr + @cmd -name punk::grepstr\ + -summary\ + "Grep for regex pattern in plaintext of supplied (possibly ANSI) string."\ + -help\ + "The grepstr command can find strings in ANSI text even if there are interspersed + ANSI colour codes etc. Even if a word has different coloured/styled letters, the + regex can match the plaintext. (Search is performed on ansistripped text, and then + the matched sections are highlighted and overlayed on the original styled/colourd + input. + If the input string has ANSI movement codes - the resultant text may not be directly + searchable because the parts of a word may be separated by various codes and other + plain text. To search such an input string, the string should first be 'rendered' to + a form where the ANSI only represents SGR styling (and perhaps other non-movement + codes) using something like overtype::renderline or overtype::rendertext." + + @leaders -min 0 -max 0 + @opts + -returnlines -type string -typesynopsis matched|all -default breaksandmatches -choicecolumns 1 -choices {matched all breaksandmatches} -choicelabels { + "matched"\ + " Return only lines that matched." + "breaksandmatches"\ + " Return configured --break= lines in between non-consecutive matches" + "all"\ + " Return all lines. + This has a similar effect to the 'grep' trick of matching on 'pattern|$' + (The $ matches all lines that have an end; ie all lines, but there is no + associated character to which to apply highlighting) + except that when instead using -returnlines all with --line-number, the * + indicator after the linenumber will only be highlighted for lines with matches, + and the following matchcount will indicate zero for non-matching lines." + } + -B|--before-context= -parsekey "--before-context" -default 0 -type integer -typesynopsis num + -C|--context= -parsekey "--context" -default 0 -type integer -typesynopsis num -help\ + "Print num lines of leading and trailing context surrounding each match." + -A|--after-context= -parsekey "--after-context" -default 0 -type integer -typesynopsis num + --break= -type string -default "-- %c%\U2260" -help\ + "When returning matched lines and there is a break in consecutive output, + display the break with the given string. %c% is a placeholder for the + number of lines skipped. + Use empty-string for an empty line as a break display. + grepstr --break= needle $haystacklines + + The unix grep utility commonly uses -- for this indicator. + grepstr --break=-- needle $haystacklines + + Customisation example: + grepstr -n \"--break=(skipped %c% lines)\" needle $haystacklines + " + -ansistrip -type none -help\ + "Strip all ansi codes from the input string before processing. + This is not necessary for regex matching purposes, as the matching is always + performed on the ansistripped characters anyway, but by stripping ANSI, the + result only has the ANSI supplied by the -highlight option." + + #-n|--line-number as per grep utility, except that we include a * for matches + -n|--line-number -type none -help\ + "Each output line is preceded by its relative line number in the file, starting at line 1. + For lines that matched the regex, the line number will be suffixed with a * indicator + with the same highlighting as the matched string(s). + The number of matches in the line immediately follows the * + For lines with no matches the * indicator is present with no highlighting and suffixed + with zeros." + -i|--ignore-case -type none -help\ + "Perform case insensitive matching." + -highlight -type list -typesynopsis ansinames -default {green bold Black underline overline} -help\ + "list of ANSI SGR style codes as supported by and documented in punk::ansi::a?" + -- -type none + @values + pattern -type string -help\ + "regex pattern to match in plaintext portion of ANSI string" + string -type string + } + proc grepstr {args} { + lassign [dict values [punk::args::parse $args withid ::punk::grepstr]] leaders opts values received + set pattern [dict get $values pattern] + set data [dict get $values string] + set do_strip 0 + if {[dict exists $received -ansistrip]} { + set data [punk::ansi::ansistrip $data] + } + set highlight [dict get $opts -highlight] + set opt_returnlines [dict get $opts -returnlines] + set context [dict get $opts --context] ;#int + set beforecontext [dict get $opts --before-context] + set beforecontext [expr {max($beforecontext,$context)}] + set aftercontext [dict get $opts --after-context] + set aftercontext [expr {max($aftercontext,$context)}] + set break [dict get $opts --break] + set ignorecase [dict exists $received --ignore-case] + if {$ignorecase} { + set nocase "-nocase" + } else { + set nocase "" + } + + + if {[dict exists $received --line-number]} { + set do_linenums 1 ;#display lineindex+1 + } else { + set do_linenums 0 + } + + if {[llength $highlight] == 0} { + set H "" + set R "" + } else { + set H [a+ {*}$highlight] + set R \x1b\[m + } + + set data [string map {\r\n \n} $data] + if {![punk::ansi::ta::detect $data]} { + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $lines $pattern] + set result "" + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + #matches|breaksandmatches + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + incr max + } + set w1 [string length $max] + #lineindex is zero based - display of linenums is 1 based + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set ln [regsub -all {*}$nocase -- $pattern $ln $H&$R] ;#n + set matchcount [regexp -all {*}$nocase -- $pattern $ln] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + } else { + if {$do_linenums} { + append col1 "*000" + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + if {$do_linenums} { + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + + } + } else { + set plain [punk::ansi::ansistrip $data] + set plainlines [split $plain \n] + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $plainlines $pattern] + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + #if max index is 9 - linenum will be 10, (99->100 etc) - so add one in case we're on such a boundary. + incr max + } + set w1 [string length $max] + set result "" + set placeholder \UFFEF ;#review + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set plain_ln [lindex $plainlines $lineindex] + set parts [regexp -all {*}$nocase -indices -inline -- $pattern $plain_ln] + set matchcount [llength $parts] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + if {[llength $parts] == 0} { + #This probably can't happen (?) + #If it does.. it's more likely to be an issue with our line index than with regexp + puts stderr "Unexpected regex mismatch in grepstr - line marked with ??? (shouldn't happen)" + set matchshow "??? $ln" + #dict set resultlines $lineindex $show + } else { + set overlay "" + set i 0 + foreach prange $parts { + lassign $prange s e + set prelen [expr {$s - $i}] + append overlay [string repeat $placeholder $prelen] $H[string range $plain_ln $s $e]$R + set i [expr {$e + 1}] + } + set tail [string range $plain_ln $e+1 end] + append overlay [string repeat $placeholder [string length $tail]] + #puts "$overlay" + #puts "$ln" + set rendered [overtype::renderline -transparent $placeholder -insert_mode 0 $ln $overlay] + if {$do_linenums} { + set matchshow "$col1 $rendered" + } else { + set matchshow $rendered + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + dict set resultlines $lineindex $matchshow + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + } else { + if {$do_linenums} { + append col1 "*000" + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + } + } + } + set ordered_resultlines [lsort -integer [dict keys $resultlines]] + set result "" + set i -1 + set do_break 0 + if {$opt_returnlines eq "breaksandmatches"} { + set do_break 1 + } + if {$do_break} { + foreach r $ordered_resultlines { + incr i + if {$r > $i} { + set c [expr {$r - $i}] + append result [string map [list %c% $c] $break] \n + } + append result [dict get $resultlines $r] \n + set i $r + } + if {$i<[llength $lines]-1} { + set c [expr {[llength $lines]-1-$i}] + append result [string map [list %c% $c] $break] \n + } + } else { + foreach r $ordered_resultlines { + append result [dict get $resultlines $r] \n + } + } + set result [string trimright $result \n] + return $result + } + proc stacktrace {} { set stack "Stack trace:\n" for {set i 1} {$i < [info level]} {incr i} { @@ -563,22 +937,24 @@ namespace eval punk { #get last command result that was run through the repl proc ::punk::get_runchunk {args} { - set argd [punk::args::get_dict { + set argd [punk::args::parse $args withdef { @id -id ::punk::get_runchunk + @cmd -name "punk::get_runchunk" -help\ + "experimental" @opts - -1 -optional 1 -type none - -2 -optional 1 -type none + -1 -optional 1 -type none + -2 -optional 1 -type none @values -min 0 -max 0 - } $args] + }] #todo - make this command run without truncating previous runchunks set runchunks [tsv::array names repl runchunks-*] set sortlist [list] foreach cname $runchunks { set num [lindex [split $cname -] 1] - lappend sortlist [list $num $cname] + lappend sortlist [list $num $cname] } - set sorted [lsort -index 0 -integer $sortlist] + set sorted [lsort -index 0 -integer $sortlist] set chunkname [lindex $sorted end-1 1] set runlist [tsv::get repl $chunkname] #puts stderr "--$runlist" @@ -635,10 +1011,10 @@ namespace eval punk { set inopts 1 } else { #leave loop at first nonoption - i should be index of file - break + break } } else { - #leave for next iteration to check + #leave for next iteration to check set inopts 0 } incr i @@ -654,44 +1030,8 @@ namespace eval punk { set ::argc $argc return -code $code $return } - #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ - # - #we can't provide a float comparison suitable for every situation, - #but we pick something reasonable, keep it stable, and document it. - proc float_almost_equal {a b} { - package require math::constants - set diff [expr {abs($a - $b)}] - if {$diff <= $math::constants::eps} { - return 1 - } - set A [expr {abs($a)}] - set B [expr {abs($b)}] - set largest [expr {($B > $A) ? $B : $A}] - return [expr {$diff <= $largest * $math::constants::eps}] - } - #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. - proc boolean_equal {a b} { - #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. - expr {($a && 1) == ($b && 1)} - } - #debatable whether boolean_almost_equal is likely to be surprising or helpful. - #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically - #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? - proc boolean_almost_equal {a b} { - if {[string is double -strict $a]} { - if {[float_almost_equal $a 0]} { - set a 0 - } - } - if {[string is double -strict $b]} { - if {[float_almost_equal $b 0]} { - set b 0 - } - } - #must handle true,no etc. - expr {($a && 1) == ($b && 1)} - } + proc varinfo {vname {flag ""}} { @@ -703,9 +1043,9 @@ namespace eval punk { error "can't read \"$vname\": no such variable" } set inf [shellfilter::list_element_info [list $v]] - set inf [dict get $inf 0] + set inf [dict get $inf 0] if {$flag eq "-v"} { - return $inf + return $inf } set output [dict create] @@ -781,7 +1121,7 @@ namespace eval punk { } else { append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -791,162 +1131,12 @@ namespace eval punk { } return $varlist } - proc splitstrposn {s p} { - if {$p <= 0} { - if {$p == 0} { - list "" $s - } else { - list $s "" - } - } else { - scan $s %${p}s%s - } - } - proc splitstrposn_nonzero {s p} { - scan $s %${p}s%s - } - #split top level of patterns only. - proc _split_patterns_memoized {varspecs} { - set name_mapped [pipecmd_namemapping $varspecs] - set cmdname ::punk::pipecmds::split_patterns::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - set result [_split_patterns $varspecs] - proc $cmdname {} [list return $result] - #debug.punk.pipe.compile {proc $cmdname} 4 - return $result - } - proc _split_patterns {varspecs} { - - set varlist [list] - # @ @@ - list and dict functions - # / level separator - # # list count, ## dict size - # % string functions - # ! not - set var_terminals [list "@" "/" "#" "%" "!" ">" "<"] ;# (> required for insertionspecs at rhs of = & .= ) - #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname - - #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# - #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string - #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' - set in_brackets 0 ;#count depth - set in_atom 0 - #set varspecs [string trimleft $varspecs ,] - set token "" - #if {[string first "," $varspecs] <0} { - # return $varspecs - #} - set first_term -1 - set token_index 0 ;#index of terminal char within each token - set indq 0 - set inesc 0 ;#whether last char was backslash (see also punk::escv) - set prevc "" - set char_index 0 - foreach c [split $varspecs ""] { - if {$indq} { - if {$inesc} { - #puts stderr "inesc adding '$c'" - append token $c - } else { - if {$c eq {"}} { - set indq 0 - } else { - append token $c - } - } - } elseif {$in_atom} { - #ignore dquotes/brackets in atoms - pass through - append token $c - #set nextc [lindex $chars $char_index+1] - if {$c eq "'"} { - set in_atom 0 - } - } elseif {$in_brackets > 0} { - append token $c - if {$c eq ")"} { - incr in_brackets -1 - } - } else { - if {$c eq {"} && !$inesc} { - set indq 1 - } elseif {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - set token "" - set token_index -1 ;#reduce by 1 because , not included in next token - set first_term -1 - } else { - append token $c - switch -exact -- $c { - ' { - set in_atom 1 - } - ( { - incr in_brackets - } - default { - if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index - } - } - } - } - } - set prevc $c - if {$c eq "\\"} { - #review - if {$inesc} { - set inesc 0 - } else { - set token [string range $token 0 end-1] - set inesc 1 - } - } else { - set inesc 0 - } - incr token_index - incr char_index - } - if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - } - return $varlist - } proc _split_var_key_at_unbracketed_comma {varspecs} { set varlist [list] set var_terminals [list "@" "/" "#" "!"] #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# + set protect_terminals [list "^"] ;# e.g sequence ^# #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' set in_brackets 0 @@ -966,27 +1156,17 @@ namespace eval punk { } } else { if {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] + set token "" set token_index -1 ;#reduce by 1 because , not included in next token set first_term -1 } else { append token $c if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index + set first_term $token_index } elseif {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -994,18 +1174,7 @@ namespace eval punk { incr token_index } if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] } return $varlist } @@ -1029,6 +1198,7 @@ namespace eval punk { } else { if {$c eq ","} { if {$first_term > -1} { + #lassign [punk::lib::string_splitbefore $token $first_term] v k set v [string range $token 0 $first_term-1] set k [string range $token $first_term end] ;#key section includes the terminal char lappend varlist [list $v $k] @@ -1041,12 +1211,12 @@ namespace eval punk { } else { if {$first_term == -1} { if {$c in $var_terminals} { - set first_term $token_index + set first_term $token_index } } append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -1067,7 +1237,7 @@ namespace eval punk { proc fp_restructure {selector data} { if {$selector eq ""} { fun=.= {val $input} 0 || abs($offset) >= $len)} { set action ?mismatch-list-index-out-of-range break @@ -1424,7 +1594,7 @@ namespace eval punk { } elseif {$start eq "end"} { #ok } elseif {$do_bounds_check} { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0 || abs($startoffset) >= $len} { set action ?mismatch-list-index-out-of-range @@ -1481,7 +1651,7 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + } else { #keyword 'pipesyntax' at beginning of error message error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] @@ -1513,23 +1683,40 @@ namespace eval punk { return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs] } - #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script + #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script proc destructure_func {selector data} { #puts stderr ".d." set selector [string trim $selector /] - #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position - #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position - - #map some problematic things out of the way in a manner that maintains some transparency - #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} - #The selector forms part of the proc name - set selector_safe [string map [list ? * {$} "" "\x1b\[" "\x1b\]" {[} {]} :: {;} " " \t \n \r ] $selector] + #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position + #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position + + #map some problematic things out of the way in a manner that maintains some transparency + #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} + #The selector forms part of the proc name + #review - compare with pipecmd_namemapping + set selector_safe [string map [list\ + ? \ + * \ + \\ \ + {"} \ + {$} \ + "\x1b\[" \ + "\x1b\]" \ + {[} \ + {]} \ + :: \ + {;} \ + " " \ + \t \ + \n \ + \r \ + ] $selector] set cmdname ::punk::pipecmds::destructure::_$selector_safe if {[info commands $cmdname] ne ""} { return [$cmdname $data] ;# note upvar 2 for stateful v_list_idx to be resolved in _multi_bind_result context } - + set leveldata $data set body [destructure_func_build_procbody $cmdname $selector $data] @@ -1553,8 +1740,8 @@ namespace eval punk { proc destructure_func_build_procbody {cmdname selector data} { set script "" #place selector in comment in script only - if there is an error in selector we pick it up when building the script. - #The script itself should only be returning errors in its action key of the result dictionary - append script \n [string map [list $selector] {# set selector {}}] + #The script itself should only be returning errors in its action key of the result dictionary + append script \n [string map [list $selector] {# set selector {}}] set subindices [split $selector /] append script \n [string map [list [list $subindices]] {# set subindices }] set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break @@ -1562,7 +1749,7 @@ namespace eval punk { #append script \n {set assigned ""} ;#review set active_key_type "" append script \n {# set active_key_type ""} - set lhs "" + set lhs "" #append script \n [tstr {set lhs ${{$lhs}}}] append script \n {set lhs ""} set rhs "" @@ -1582,9 +1769,9 @@ namespace eval punk { #dict 'index' when using stateful @@ etc to iterate over dict instead of by key set tpl_return_mismatch_dict_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-dict-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} set tpl_return_mismatch_dict_key_not_found {return [dict create -assigned $leveldata -action ?mismatch-dict-key-not-found -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} - - if {![string length $selector]} { + + if {![string length $selector]} { #just return $leveldata set script { dict create -assigned $leveldata -action ?match -lhs "" -rhs $leveldata @@ -1598,7 +1785,7 @@ namespace eval punk { #pure numeric keylist - put straight to lindex # #NOTE: this direct access e.g v/0/1/2 doesn't check out of bounds which is at odds with list access containing @ - #We will leave this as a syntax for different (more performant) behaviour + #We will leave this as a syntax for different (more performant) behaviour #- it's potentially a little confusing - but it would be a shame not to have the possibility to take advantage of the lindex deep indexing capability in pattern matching. #TODO - review and/or document # @@ -1625,7 +1812,7 @@ namespace eval punk { # -- --- --- } if {[string match @@* $selector]} { - #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc + #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc set rawkeylist [split $selector /] ;#first key retains @@ - may be just '@@' set keypath [string range $selector 2 end] set keylist [split $keypath /] @@ -1659,11 +1846,11 @@ namespace eval punk { foreach index $subindices { #set index_operation "unspecified" set level_script_complete 0 ;#instead of break - as we can't use data to determine break when building script - set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] + set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] append script \n "# ------- START index:$index subpath:$SUBPATH ------" set lhs $index - append script \n "set lhs $index" - + append script \n "set lhs {$index}" + set assigned "" append script \n {set assigned ""} @@ -1677,21 +1864,21 @@ namespace eval punk { # do_bounds_check shouldn't need to be in script set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning. - #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. + #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. #append script \n {set do_boundscheck 0} switch -exact -- $index { # - @# { #list length set active_key_type "list" if {$get_not} { - lappend INDEX_OPERATIONS not-list + lappend INDEX_OPERATIONS not-list append script \n {# set active_key_type "list" index_operation: not-list} append script \n { if {[catch {llength $leveldata}]} { - #not a list - not-length is true + #not a list - not-length is true set assigned 1 } else { - #is a list - not-length is false + #is a list - not-length is false set assigned 0 } } @@ -1710,7 +1897,7 @@ namespace eval punk { #dict size set active_key_type "dict" if {$get_not} { - lappend INDEX_OPERATIONS not-dict + lappend INDEX_OPERATIONS not-dict append script \n {# set active_key_type "dict" index_operation: not-dict} append script \n { if {[catch {dict size $leveldata}]} { @@ -1733,10 +1920,10 @@ namespace eval punk { } %# { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS string-length append script \n {# set active_key_type "" index_operation: string-length} append script \n {set assigned [string length $leveldata]} @@ -1745,10 +1932,10 @@ namespace eval punk { %%# { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS ansistring-length append script \n {# set active_key_type "" index_operation: ansistring-length} append script \n {set assigned [ansistring length $leveldata]} @@ -1756,7 +1943,7 @@ namespace eval punk { } %str { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%str - not string-get is not supported" } lappend INDEX_OPERATIONS string-get @@ -1767,7 +1954,7 @@ namespace eval punk { %sp { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%sp - not string-space is not supported" } lappend INDEX_OPERATIONS string-space @@ -1778,7 +1965,7 @@ namespace eval punk { %empty { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%empty - not string-empty is not supported" } lappend INDEX_OPERATIONS string-empty @@ -1788,10 +1975,10 @@ namespace eval punk { } @words { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%words - not list-words-from-string is not supported" } - lappend INDEX_OPERATIONS list-words-from-string + lappend INDEX_OPERATIONS list-words-from-string append script \n {# set active_key_type "" index_operation: list-words-from-string} append script \n {set assigned [regexp -inline -all {\S+} $leveldata]} set level_script_complete 1 @@ -1800,10 +1987,10 @@ namespace eval punk { #experimental - leading character based on result not input(?) #input type is string - but output is list set active_key_type "list" - if $get_not { + if {$get_not} { error "!%chars - not list-chars-from-string is not supported" } - lappend INDEX_OPERATIONS list-from_chars + lappend INDEX_OPERATIONS list-from_chars append script \n {# set active_key_type "" index_operation: list-chars-from-string} append script \n {set assigned [split $leveldata ""]} set level_script_complete 1 @@ -1812,7 +1999,7 @@ namespace eval punk { #experimental - flatten one level of list #join without arg - output is list set active_key_type "string" - if $get_not { + if {$get_not} { error "!@join - not list-join-list is not supported" } lappend INDEX_OPERATIONS list-join-list @@ -1824,7 +2011,7 @@ namespace eval punk { #experimental #input type is list - but output is string set active_key_type "string" - if $get_not { + if {$get_not} { error "!%join - not string-join-list is not supported" } lappend INDEX_OPERATIONS string-join-list @@ -1834,7 +2021,7 @@ namespace eval punk { } %ansiview { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiview is not supported" } lappend INDEX_OPERATIONS string-ansiview @@ -1844,7 +2031,7 @@ namespace eval punk { } %ansiviewstyle { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiviewstyle is not supported" } lappend INDEX_OPERATIONS string-ansiviewstyle @@ -1855,23 +2042,23 @@ namespace eval punk { @ { #as this is a stateful list next index operation - we use not (!@) to mean there is no element at the next index (instead of returning the complement ie all elements except next) #This is in contrast to other not operations on indices e.g /!2 which returns all elements except that at index 2 - + #append script \n {puts stderr [uplevel 1 [list info vars]]} #NOTE: #v_list_idx in context of _multi_bind_result - #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) + #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) append script \n {upvar 2 v_list_idx v_list_idx} set active_key_type "list" append script \n {# set active_key_type "list" index_operation: list-get-next} #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3 - #while x@,y@.= is reasonably handy - especially for args e.g $keyglob] { # set active_key_type "dict" index_operation: globkey-get-pairs-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict remove $leveldata {*}$matched] }] @@ -2285,7 +2473,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-pairs append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operations: globkey-get-pairs - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict create] foreach m $matched { dict set assigned $m [dict get $leveldata $m] @@ -2307,7 +2495,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys-not append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict keys [dict remove $leveldata {*}$matched]] }] @@ -2315,7 +2503,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys - set assigned [dict keys $leveldata ] + set assigned [dict keys $leveldata {}] }] } set level_script_complete 1 @@ -2323,7 +2511,7 @@ namespace eval punk { {@k\*@*} - {@K\*@*} { #dict value glob - return keys set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2331,22 +2519,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-keys-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match "" $v]} { + if {![string match {} $v]} { lappend assigned $k } } }] } else { lappend INDEX_OPERATIONS globvalue-get-keys - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys set assigned [list] tcl::dict::for {k v} $leveldata { - if {[string match "" $v]} { + if {[string match {} $v]} { lappend assigned $k } } @@ -2357,7 +2545,7 @@ namespace eval punk { {@.\*@*} { #dict value glob - return pairs set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2365,22 +2553,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-pairs-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { dict set assigned $k $v } } }] } else { lappend INDEX_OPERATIONS globvalue-get-pairs - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $v]} { + if {[string match {} $v]} { dict set assigned $k $v } } @@ -2389,9 +2577,9 @@ namespace eval punk { set level_script_complete 1 } {@V\*@*} - {@v\*@*} { - #dict value glob - return values + #dict value glob - return values set active_key_type dict - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2399,11 +2587,11 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-values-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;# index_operation: globvalue-get-values-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { lappend assigned $v } } @@ -2411,9 +2599,9 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS globvalue-get-values - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;#index_operation: globvalue-get-value - set assigned [dict values $leveldata ] + set assigned [dict values $leveldata ] }] } set level_script_complete 1 @@ -2437,14 +2625,14 @@ namespace eval punk { # set active_key_type "dict" ;# index_operation: globkeyvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $k] || [string match $v]} { + if {[string match {} $k] || [string match {} $v]} { dict set assigned $k $v } } }] } - - error "globkeyvalue-get-pairs todo" + set level_script_complete 1 + puts stderr "globkeyvalue-get-pairs review" } @* { set active_key_type "list" @@ -2483,7 +2671,7 @@ namespace eval punk { append listmsg "Use var@@key to treat value as a dict and retrieve element at key" #append script \n [string map [list $listmsg] {set listmsg ""}] - + #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against @@ -2544,7 +2732,7 @@ namespace eval punk { ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax - ${$assignment_script} + ${$assignment_script} } }] } @@ -2568,7 +2756,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { - ${$assignment_script} + ${$assignment_script} } }] } else { @@ -2577,13 +2765,13 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assignment_script} + ${$assignment_script} } }] } } tail { - #NOTE: /@tail and /tail both do bounds check. This is intentional. + #NOTE: /@tail and /tail both do bounds check. This is intentional. # #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems. @@ -2596,7 +2784,7 @@ namespace eval punk { append script \n "# index_operation listindex-tail" \n lappend INDEX_OPERATIONS listindex-tail set assignment_script {set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero} - } + } append script \n [tstr -return string -allowcommands { if {[catch {llength $leveldata} len]} { #set action ?mismatch-not-a-list @@ -2693,7 +2881,7 @@ namespace eval punk { } raw { #get_not - return nothing?? - #no list checking.. + #no list checking.. if {$get_not} { lappend INDEX_OPERATIONS getraw-not append script \n {set assigned {}} @@ -2748,7 +2936,7 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS list-getpairs } - append script \n [tstr -return string -allowcommands { + append script \n [tstr -return string -allowcommands { if {[catch {dict size $leveldata} dsize]} { #set action ?mismatch-not-a-dict ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2776,7 +2964,7 @@ namespace eval punk { if {[catch {llength $leveldata} len]} { ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } elseif {[string is integer -strict $index]} { @@ -2816,7 +3004,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2847,7 +3035,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} } else { - ${$assign_script} + ${$assign_script} } } }] @@ -2857,7 +3045,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2896,15 +3084,15 @@ namespace eval punk { } elseif {$start eq "end"} { #noop } else { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0} { #e.g end+1 error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } - append script \n [tstr -return string -allowcommands { - set startoffset ${$startoffset} + append script \n [tstr -return string -allowcommands { + set startoffset ${$startoffset} if {abs($startoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2916,7 +3104,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set end ${$end} + set end ${$end} if {$end+1 > $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2932,7 +3120,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set endoffset ${$endoffset} + set endoffset ${$endoffset} if {abs($endoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -3014,13 +3202,13 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + append script \n [string map [list $assign_script] { if {![string match ?mismatch-* $action]} { } }] - + } else { #keyword 'pipesyntax' at beginning of error message #pipesyntax error - no need to even build script - can fail now @@ -3072,7 +3260,7 @@ namespace eval punk { #dict remove can accept non-existent keys.. review do we require not-@?@key to get silence? append script \n [tstr -return string { set assigned [dict remove $leveldata ${$index}] - }] + }] } else { append script \n [tstr -return string -allowcommands { # set active_key_type "dict" @@ -3096,7 +3284,7 @@ namespace eval punk { } incr i_keyindex append script \n "# ------- END index $index ------" - } ;# end foreach + } ;# end foreach @@ -3109,157 +3297,6 @@ namespace eval punk { return $script } - #todo - recurse into bracketed sub parts - #JMN3 - #e.g @*/(x@0,y@2) - proc _var_classify {multivar} { - set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - - #comma seems a natural choice to split varspecs, - #but also for list and dict subelement access - #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) - #so / will indicate subelements e.g @0/1 for lindex $list 0 1 - #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] - set valsource_key_list [_split_patterns_memoized $multivar] - - - - #mutually exclusive - atom/pin - #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin - #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] - #0 - novar - #1 - atom ' - #2 - pin ^ - #3 - boolean & - #4 - integer - #5 - double - #6 - var - #7 - glob (no classifier and contains * or ?) - #8 - numeric - #9 - > (+) - #10 - < (-) - - set var_names [list] - set var_class [list] - set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob - - - set leading_classifiers [list "'" "&" "^" ] - set trailing_classifiers [list + -] - set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] - - foreach v_key $valsource_key_list { - lassign $v_key v key - set vname $v ;#default - set classes [list] - if {$v eq ""} { - lappend var_class [list $v_key 0] - lappend varspecs_trimmed $v_key - } else { - set lastchar [string index $v end] - switch -- $lastchar { - + { - lappend classes 9 - set vname [string range $v 0 end-1] - } - - { - lappend classes 10 - set vname [string range $v 0 end-1] - } - } - set firstchar [string index $v 0] - switch -- $firstchar { - ' { - lappend var_class [list $v_key 1] - #set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - ^ { - lappend classes [list 2] - #use vname - may already have trailing +/- stripped - set vname [string range $vname 1 end] - set secondclassifier [string index $v 1] - switch -- $secondclassifier { - "&" { - #pinned boolean - lappend classes 3 - set vname [string range $v 2 end] - } - "#" { - #pinned numeric comparison instead of string comparison - #e.g set x 2 - # this should match: ^#x.= list 2.0 - lappend classes 8 - set vname [string range $vname 1 end] - } - "*" { - #pinned glob - lappend classes 7 - set vname [string range $v 2 end] - } - } - #todo - check for second tag - & for pinned boolean? - #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. - #while we're at it.. pinned glob would be nice. ^* - #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. - #These all limit the range of varnames permissible - which is no big deal. - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed [list $vname $key] - } - & { - #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. - #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans - #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. - lappend var_class [list $v_key 3] - set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - default { - if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { - lappend var_class [list $v_key 7] ;#glob - #leave vname as the full glob - lappend varspecs_trimmed [list "" $key] - } else { - #scan vname not v - will either be same as v - or possibly stripped of trailing +/- - set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 - #leading . still need to test directly for double - if {[string is double -strict $vname] || [string is double -strict $numtestv]} { - if {[string is integer -strict $numtestv]} { - #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired - #integer test before double.. - #note there is also string is wide (string is wideinteger) for larger ints.. - lappend classes 4 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } else { - #double - #sci notation 1e123 etc - #also large numbers like 1000000000 - even without decimal point - (tcl bignum) - lappend classes 5 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } - } else { - lappend var_class [list $v_key 6] ;#var - lappend varspecs_trimmed $v_key - } - } - } - } - } - lappend var_names $vname - } - - set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] - - proc $cmdname {} [list return $result] - debug.punk.pipe.compile {proc $cmdname} - return $result - } @@ -3269,41 +3306,41 @@ namespace eval punk { #TODO - implement cross-binding (as opposed to overwrite/reassignment) when a var appears multiple times in a pattern/multivar #e.g x@0,x@1 will only match if value at positions 0 & 1 is the same (a form of auto-pinning?) #e.g x,x@0 will only match a single element list - #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) + #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) # non-blocking or - p1||p2 if p1 matches - return p1 and continue pipeline - else match p2 and continue pipeline proc _multi_bind_result {multivar data args} { #puts stdout "---- _multi_bind_result multivar:'$multivar' data:'$data' options:'$args'" - #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 + #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 if {![string length $multivar]} { #treat the absence of a pattern as a match to anything #JMN2 - changed to list based destructuring return [dict create ismatch 1 result $data setvars {} script {}] #return [dict create ismatch 1 result [list $data] setvars {} script {}] } - set returndict [dict create ismatch 0 result "" setvars {}] - set script "" + set returndict [dict create ismatch 0 result "" setvars {}] + set script "" - set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] - set opts [dict merge $defaults $args] - set unset [dict get $opts -unset] - set lvlup [dict get $opts -levelup] - set get_mismatchinfo [dict get $opts -mismatchinfo] + set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] + set opts [dict merge $defaults $args] + set unset [dict get $opts -unset] + set lvlup [dict get $opts -levelup] + set get_mismatchinfo [dict get $opts -mismatchinfo] #first classify into var_returntype of either "pipeline" or "segment" #segment returntype is indicated by leading % - set varinfo [_var_classify $multivar] - set var_names [dict get $varinfo var_names] - set var_class [dict get $varinfo var_class] - set varspecs_trimmed [dict get $varinfo varspecs_trimmed] + set varinfo [punk::pipe::lib::_var_classify $multivar] + set var_names [dict get $varinfo var_names] + set var_class [dict get $varinfo var_class] + set varspecs_trimmed [dict get $varinfo varspecs_trimmed] set var_actions [list] set expected_values [list] #e.g {a = abc} {b set ""} foreach classinfo $var_class vname $var_names { - lassign [lindex $classinfo 0] v + lassign [lindex $classinfo 0] v lappend var_actions [list $v "" ""] ;#varactions keeps original lhs - not trimmed version lappend expected_values [list var $vname spec $v info - lhs - rhs -] ;#code looks for 'info -' to see if changed from default } @@ -3314,7 +3351,7 @@ namespace eval punk { #puts stdout "\n var_class: $var_class\n" # e.g {{x {}} 0} {{y @0} 0} {{'ok @0} 1} {{^v @@key} 2} - + #set varspecs_trimmed [lmap varinfo $var_class {expr {([lindex $varinfo 1] > 0) ? [list [string range [lindex $varinfo 0 0] 1 end] [lindex $varinfo 0 1]] : [lindex $varinfo 0]}}] #puts stdout "\n varspecs_trimmed: $varspecs_trimmed\n" @@ -3329,18 +3366,18 @@ namespace eval punk { #member lists of returndict which will be appended to in the initial value-retrieving loop set returndict_setvars [dict get $returndict setvars] - + set assigned_values [list] #varname action value - where value is value to be set if action is set - #actions: + #actions: # "" unconfigured - assert none remain unconfigured at end # noop no-change # matchvar-set name is a var to be matched # matchatom-set names is an atom to be matched # matchglob-set - # set + # set # question mark versions are temporary - awaiting a check of action vs var_class # e.g ?set may be changed to matchvar or matchatom or set @@ -3355,7 +3392,7 @@ namespace eval punk { # ^var means a pinned variable - compare value of $var to rhs - don't assign # # In this loop we don't set variables - but assign an action entry in var_actions - all with leading question mark. - # as well as adding the data values to the var_actions list + # as well as adding the data values to the var_actions list # # TODO! we may (commonly) encounter same vkey in the pattern - no need to reparse and re-fetch from data! set vkeys_seen [list] @@ -3396,8 +3433,8 @@ namespace eval punk { dict set returndict setvars $returndict_setvars #assigned_values is the ordered list of source elements in the data (rhs) as extracted by each position-spec - #For booleans the final val may later be normalised to 0 or 1 - + #For booleans the final val may later be normalised to 0 or 1 + #assertion all var_actions were set with leading question mark #perform assignments only if matched ok @@ -3424,7 +3461,7 @@ namespace eval punk { debug.punk.pipe.var {vars: [lsearch -all -inline -index 1 $var_class 6]} 5 debug.punk.pipe.var {globs: [lsearch -all -inline -index 1 $var_class 7]} 5 } - + set match_state [lrepeat [llength $var_names] ?] unset -nocomplain v unset -nocomplain nm @@ -3445,7 +3482,7 @@ namespace eval punk { set class_key [lindex $var_class $i 1] - lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan + lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan foreach ck $class_key { switch -- $ck { 1 {set isatom 1} @@ -3473,7 +3510,7 @@ namespace eval punk { ##marking numbers with pin ^ has undetermined meaning. Perhaps force expr matching only? #set isgreaterthan [expr {9 in $class_key}] #set islessthan [expr {10 in $class_key}] - + if {$isatom} { @@ -3502,7 +3539,7 @@ namespace eval punk { # - setting expected_values when match_state is set to 0 is ok except for performance - #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or + #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or #ispin may reclassify as isint,isdouble based on contained value (as they don't have their own classifier char and are unambiguous and require special handling) if {$ispin} { #puts stdout "==>ispin $lhsspec" @@ -3512,7 +3549,7 @@ namespace eval punk { upvar $lvlup $varname the_var #if {![catch {uplevel $lvlup [list ::set $varname]} existingval]} {} if {![catch {set the_var} existingval]} { - + if {$isbool} { #isbool due to 2nd classifier i.e ^& lset expected_values $i [list var $varname spec $lhsspec info test-lhs-bool lhs $existingval rhs $val] @@ -3522,7 +3559,7 @@ namespace eval punk { #isglob due to 2nd classifier ^* lset expected_values $i [list var $varname spec $lhsspec info test-lhs-glob lhs $existingval rhs $val] } elseif {$isnumeric} { - #flagged as numeric by user using ^# classifiers + #flagged as numeric by user using ^# classifiers set testexistingval [join [scan $existingval %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, internal decimal points and sci notation (but not leading .) if {[string is integer -strict $testexistingval]} { set isint 1 @@ -3533,10 +3570,10 @@ namespace eval punk { set isdouble 1 #doubles comparisons use float_almost_equal - so lhs can differ from rhs - for pins we always want to return the normalised lhs ie exactly what is in the var lset assigned_values $i $existingval - + lset expected_values $i [list var $varname spec $lhsspec info test-lhs-double lhs $existingval rhs $val] } else { - #user's variable doesn't seem to have a numeric value + #user's variable doesn't seem to have a numeric value lset match_state $i 0 lset expected_values $i [list var $varname spec $lhsspec info mismatch-lhs-not-numeric lhs $existingval rhs $val] break @@ -3561,7 +3598,7 @@ namespace eval punk { lset expected_values $i [list var $varname spec $lhsspec info failread-$varname lhs ? rhs $val] break } - } + } } @@ -3583,7 +3620,7 @@ namespace eval punk { if {[string index $lhs 0] eq "."} { set testlhs $lhs } else { - set testlhs [join [scan $lhs %lld%s] ""] + set testlhs [join [scan $lhs %lld%s] ""] } if {[string index $val 0] eq "."} { set testval $val @@ -3648,10 +3685,10 @@ namespace eval punk { } } elseif {[string is digit -strict [string trim $val -]] } { #probably a wideint or bignum with no decimal point - #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . + #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . #if we use float_almost_equal they may compare equal. on the other hand expr also does apparently inconsistent thins with comparing integer-like bignums vs similar sized nums with .x at the end. - #2 values further apart can compare equal while int-like ones closer together can compare different. - #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. + #2 values further apart can compare equal while int-like ones closer together can compare different. + #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. #This is basically what we're doing here but with an arguably better (for some purposes!) float comparison. #string comparison can presumably always be used as an alternative. # @@ -3682,7 +3719,7 @@ namespace eval punk { } } } else { - if {[punk::float_almost_equal $testlhs $testval]} { + if {[punk::pipe::float_almost_equal $testlhs $testval]} { lset match_state $i 1 } else { if {$isgreaterthan} { @@ -3709,7 +3746,7 @@ namespace eval punk { } } } else { - #e.g rhs not a number.. + #e.g rhs not a number.. if {$testlhs == $testval} { lset match_state $i 1 } else { @@ -3721,7 +3758,7 @@ namespace eval punk { } elseif {$isdouble} { #dragons (and shimmering) # - # + # if {$ispin} { set existing_expected [lindex $expected_values $i] set lhs [dict get $existing_expected lhs] @@ -3761,7 +3798,7 @@ namespace eval punk { } } else { #float_almost_equal will disagree with expr based on scale.. just enough to allow for example [expr 0.2 + 0.1] to equal 0.3 - whereas expr will declare a mismatch - if {[punk::float_almost_equal $lhs $testval]} { + if {[punk::pipe::float_almost_equal $lhs $testval]} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-float-almost-equal lhs $lhs rhs $val] } else { @@ -3777,7 +3814,7 @@ namespace eval punk { # Also - the setting of the variable x is normalized to 1 or 0 only. (true & false would perhaps be nicer - but 1 & 0 are theoretically more efficient for later comparisons as they can have a pure int rep?.) # todo - consider the booleanString rep. Can/should we return true & false instead and maintain efficiency w.r.t shimmering? # - #punk::boolean_equal $a $b + #punk::pipe::boolean_equal $a $b set extra_match_info "" ;# possible crossbind indication set is_literal_boolean 0 if {$ispin} { @@ -3789,7 +3826,7 @@ namespace eval punk { set lhs [string range $lhsspec 1 end] ;# - strip off & classifier prefix if {![string length $lhs]} { - #empty varname - ok + #empty varname - ok if {[string is boolean -strict $val] || [string is double -strict $val]} { lset match_state $i 1 lset var_actions $i 1 "return-normalised-value" @@ -3813,7 +3850,7 @@ namespace eval punk { set tclvar $lhs if {[string is double $tclvar]} { error "pipesyntax invalid variable name '$tclvar' for boolean in pattern. (subset of legal tcl vars allowed in pattern context)" "_multi_bind_result $multivar $data $args" [list pipesyntax patternvariable invalid_boolean $tclvar] - #proc _multi_bind_result {multivar data args} + #proc _multi_bind_result {multivar data args} } #treat as variable - need to check cross-binding within this pattern group set first_bound [lsearch -index 0 $var_actions $lhsspec] @@ -3846,7 +3883,7 @@ namespace eval punk { #may have already matched above..(for variable) if {[lindex $match_state $i] != 1} { - if {![catch {punk::boolean_almost_equal $lhs $val} ismatch]} { + if {![catch {punk::pipe::boolean_almost_equal $lhs $val} ismatch]} { if {$ismatch} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-boolean-almost-equal$extra_match_info lhs $lhs rhs $val] @@ -3880,11 +3917,11 @@ namespace eval punk { } } elseif {$ispin} { - #handled above.. leave case in place so we don't run else for pins + #handled above.. leave case in place so we don't run else for pins } else { #puts stdout "==> $lhsspec" - #NOTE - pinned var of same name is independent! + #NOTE - pinned var of same name is independent! #ie ^x shouldn't look at earlier x bindings in same pattern #unpinned non-atoms #cross-binding. Within this multivar pattern group only (use pin ^ for binding to result from a previous pattern) @@ -3904,7 +3941,7 @@ namespace eval punk { } default { set first_bound [lsearch -index 0 $var_actions $varname] - #assertion first_bound >=0, we will always find something - usually self + #assertion first_bound >=0, we will always find something - usually self if {$first_bound == $i} { lset match_state $i 1 lset var_actions $i 1 [string range $act 1 end] ;# ?set -> set @@ -3964,7 +4001,7 @@ namespace eval punk { if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { #isvar if {[lindex $var_actions $i 1] eq "set"} { - upvar $lvlup $varname the_var + upvar $lvlup $varname the_var set the_var [lindex $var_actions $i 2] } } @@ -3976,7 +4013,7 @@ namespace eval punk { # if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { # #isvar # lassign $va lhsspec act val - # upvar $lvlup $varname the_var + # upvar $lvlup $varname the_var # if {$act eq "set"} { # set the_var $val # } @@ -3990,7 +4027,8 @@ namespace eval punk { #todo - some way to restrict mismatch info to simple "mismatch" and avoid overhead of verbose message #e.g for within pipeswitch block where mismatches are expected and the reasons are less important than moving on quickly set vidx 0 - set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + #set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + set mismatches [lmap m $match_state v $var_names {expr {$m == 0 ? [list mismatch $v] : [list match $v]}}] set var_display_names [list] foreach v $var_names { if {$v eq ""} { @@ -3999,7 +4037,9 @@ namespace eval punk { lappend var_display_names $v } } - set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + #REVIEW 2025 + #set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0 ? $v : [expr {$m eq "?" ? "?[string repeat { } [expr {[string length $v] -1}]]" : [string repeat " " [string length $v]] }]}}] set msg "\n" append msg "Unmatched\n" append msg "Cannot match right hand side to pattern $multivar\n" @@ -4015,12 +4055,12 @@ namespace eval punk { #6 - var #7 - glob (no classifier and contains * or ?) foreach mismatchinfo $mismatches { - lassign $mismatchinfo status varname + lassign $mismatchinfo status varname if {$status eq "mismatch"} { # varname can be empty string set varclass [lindex $var_class $i 1] set val [lindex $var_actions $i 2] - set e [dict get [lindex $expected_values $i] lhs] + set e [dict get [lindex $expected_values $i] lhs] set type "" if {2 in $varclass} { append type "pinned " @@ -4098,7 +4138,7 @@ namespace eval punk { return [dict get $d result] } } - # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch + # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch proc _handle_bind_result_experimental1 {d} { #set match_caller [info level 2] #debug.punk.pipe {_handle_bind_result match_caller: $match_caller} 9 @@ -4122,55 +4162,43 @@ namespace eval punk { upvar $pipevarname the_pipe set the_pipe $args } - + #pipealias should capture the namespace context of the pipeline so that commands are resolved in the namespace in which the pipealias is created proc pipealias {targetcmd args} { set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] - tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] + tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] } proc pipealias_extract {targetcmd} { set applybody [lindex [interp alias "" $targetcmd] 1 1] #strip off trailing " {*}$args" - return [lrange [string range $applybody 0 end-9] 0 end] + return [lrange [string range $applybody 0 end-9] 0 end] } #although the pipealias2 'concat' alias is cleaner in that the original pipeline can be extracted using list commands - it runs much slower proc pipealias2 {targetcmd args} { - set cmdcopy [punk::objclone $args] + set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] } - #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) - # (for .= and = pipecmds) - proc pipecmd_namemapping {rhs} { - #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. - #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence - #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test - set rhs [string trim $rhs];#ignore all leading & trailing whitespace - set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token - set rhs [tcl::string::map {: ? * } $rhs] - #review - we don't expect other command-incompatible chars such as colon? - return $rhs - } #same as used in unknown func for initial launch - #variable re_assign {^([^\r\n=\{]*)=(.*)} - #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} variable re_assign {^([^ \t\r\n=\{]*)=(.*)} variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #match_assign is tailcalled from unknown - uplevel 1 gets to caller level proc match_assign {scopepattern equalsrhs args} { - #review - :: is legal in atoms! + #review - :: is legal in atoms! if {[string match "*::*" $scopepattern]} { error "match_assign scopepattern '$scopepattern' contains namespace separator '::' - invalid." } #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args" set fulltail $args set cmdns ::punk::pipecmds - set namemapping [pipecmd_namemapping $equalsrhs] + set namemapping [punk::pipe::lib::pipecmd_namemapping $equalsrhs] - #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW + #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW #(we need for example x*= to be available as is via namespace path mechanism (from punk::pipecmds namespace)) set pipecmd ${cmdns}::$scopepattern=$namemapping @@ -4189,10 +4217,10 @@ namespace eval punk { #NOTE: #we need to ensure for case: - #= x=y + #= x=y #that the second arg is treated as a raw value - never a pipeline command - #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = + #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = #debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4 #can match pattern on lhs with a value where pattern is a minilang that can refer to atoms (simple non-whitespace strings), numbers, or varnames (possibly pinned) as well as a trailing spec for position within the data. @@ -4202,7 +4230,7 @@ namespace eval punk { # in our script's handling of args: #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists - #same with lsearch with a string pattern - + #same with lsearch with a string pattern - #wouldn't matter for small lists - but we need to be able to handle large ones efficiently without unneccessary string reps set script [string map [list $scopepattern $equalsrhs] { #script built by punk::match_assign @@ -4210,7 +4238,7 @@ namespace eval punk { #scan for existence of any pipe operator (|*> or <*|) only - we don't need position #all pipe operators must be a single element #we don't first check llength args == 1 because for example: - # x= <| + # x= <| # x= |> #both leave x empty. To assign a pipelike value to x we would have to do: x= <| |> (equiv: set x |>) foreach a $args { @@ -4239,14 +4267,14 @@ namespace eval punk { # as we aren't in a pipleine - there is no data to insert - we proably still need to run _split_equalsrhs to verify the syntax. # review - consider way to turn it off as optimisation for non-pipelined assignment - but generally standard Tcl set could be used for that purpose. # We are probably only here if testing in the repl - in which case the error messages are important. - set var_index_position_list [_split_equalsrhs $equalsrhs] + set var_index_position_list [punk::pipe::lib::_split_equalsrhs $equalsrhs] #we may have an insertion-spec that inserts a literal atom e.g to wrap in "ok" # x='ok'>0/0 data # => {ok data} - # we won't examine for vars as there is no pipeline - ignore + # we won't examine for vars as there is no pipeline - ignore # also ignore trailing * (indicator for variable data to be expanded or not - ie {*}) # we will differentiate between / and @ in the same way that general pattern matching works. - # /x will simply call linsert without reference to length of list + # /x will simply call linsert without reference to length of list # @x will check for out of bounds # # !TODO - sort by position lowest to highest? or just require user to order the pattern correctly? @@ -4259,7 +4287,7 @@ namespace eval punk { #Here, we are not assigning to v1 - but matching the index spec /0 with the data from v1 #ie Y is inserted at position 0 to get A Y #(Note the difference from lhs) - #on lhs v1/1= {X Y} + #on lhs v1/1= {X Y} #would pattern match against the *data* A B and set v1 to B #in this point of an assign (= as opposed to .=) IF we have already determined there is no trailing pipeline @@ -4268,10 +4296,10 @@ namespace eval punk { #eg out= list a $callervar c #or alternatively use .= instead # - #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments + #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments #At the moment - this is handled in the script above by diverting to punk::pipeline to handle #The only vars/data we can possibly have to insert, come from the ] }] - set needs_insertion 0 + set needs_insertion 0 } if {$needs_insertion} { set script2 [punk::list_insertion_script $positionspec segmenttail ] set script2 [string map [list "\$insertion_data" ] $script2] append script $script2 - } + } + - } } - if {![string length $scopepattern]} { + if {![string length $scopepattern]} { append script { return $segmenttail } } else { append script [string map [list $scopepattern] { #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail - set d [punk::_multi_bind_result {} $segmenttail] + set d [punk::_multi_bind_result {} $segmenttail] #return [punk::_handle_bind_result $d] - #maintenance: inlined + #maintenance: inlined if {![dict exists $d result]} { #uplevel 1 [list error [dict get $d mismatch]] #error [dict get $d mismatch] @@ -4356,7 +4384,7 @@ namespace eval punk { tailcall $pipecmd {*}$args } - #return a script for inserting data into listvar + #return a script for inserting data into listvar #review - needs updating for list-return semantics of patterns? proc list_insertion_script {keyspec listvar {data }} { set positionspec [string trimright $keyspec "*"] @@ -4384,15 +4412,15 @@ namespace eval punk { } elseif {$isint || [regexp {^(end|end[-+]{1,2}[0-9]+)$} $index]} { if {$ptype eq "@"} { #compare position to *possibly updated* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index) - if {$isint} { + if {$isint} { append script [string map [list $listvar $index] { if {( > [llength $])} { - #not a pipesyntax error + #not a pipesyntax error error "pipedata insertionpattern index out of bounds. index: vs len: [llength $] use /x instead of @x to avoid check (list_insertion_script)" "list_insertion_script $keyspec" [list pipedata insertionpattern index_out_f_bounds] } }] } - #todo check end-x bounds? + #todo check end-x bounds? } if {$isint} { append script [string map [list $listvar $index $exp $data] { @@ -4455,98 +4483,20 @@ namespace eval punk { }] } - + } else { error "pipesyntax error in segment - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)4" "list_insertion_script $keyspec" [list pipesyntax insertionpattern_invalid] - } + } return $script } - #todo - consider whether we can use < for insertion/iteration combinations - # =a<,b< iterate once through - # =a><,b>< cartesian product - # =a<>,b<> ??? zip ? - # - # ie = {a b c} |> .=< inspect - # would call inspect 3 times, once for each argument - # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list - # would produce list of cartesian pairs? - # - proc _split_equalsrhs {insertionpattern} { - #map the insertionpattern so we can use faster globless info command search - set name_mapped [pipecmd_namemapping $insertionpattern] - set cmdname ::punk::pipecmds::split_rhs::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] - set i 0 - set return_triples [list] - foreach v_pos $lst_var_indexposition { - lassign $v_pos v index_and_position - #e.g varname@@data/ok>0 varname/1/0>end - #ensure only one ">" is detected - if {![string length $index_and_position]} { - set indexspec "" - set positionspec "" - } else { - set chars [split $index_and_position ""] - set posns [lsearch -all $chars ">"] - if {[llength $posns] > 1} { - error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - if {![llength $posns]} { - set indexspec $index_and_position - set positionspec "" - } else { - set splitposn [lindex $posns 0] - set indexspec [string range $index_and_position 0 $splitposn-1] - set positionspec [string range $index_and_position $splitposn+1 end] - } - } - - #review - - if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { - set star "" - if {$v eq "*"} { - set v "" - set star "*" - } - if {[string index $positionspec end] eq "*"} { - set star "*" - } - #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent - #as are /end and @end - #lset lst_var_indexposition $i [list $v "/end$star"] - set triple [list $v $indexspec "/end$star"] - } else { - if {$positionspec eq ""} { - #e.g just =varname - #lset lst_var_indexposition $i [list $v "/end"] - set triple [list $v $indexspec "/end"] - #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" - } else { - if {[string index $indexspec 0] ni [list "" "/" "@"]} { - error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - set triple [list $v $indexspec $positionspec] - } - } - lappend return_triples $triple - incr i - } - proc $cmdname {} [list return $return_triples] - return $return_triples - } - proc _is_math_func_prefix {e1} { #also catch starting brackets.. e.g "(min(4,$x) " if {[regexp {^[^[:alnum:]]*([[:alnum:]]*).*} $e1 _ word]} { - #possible math func + #possible math func if {$word in [info functions]} { return true } @@ -4583,8 +4533,8 @@ namespace eval punk { #puts "PERCENTS : $percents" set sequences [list] set in_sequence 0 - set start -1 - set end -1 + set start -1 + set end -1 set i 0 #todo - some more functional way of zipping/comparing these lists? set s_length 0 ;#sequence length including % symbols - minimum for tag therefore 2 @@ -4601,7 +4551,7 @@ namespace eval punk { } else { if {$n ^ $p} { incr s_length - incr end + incr end } else { if {$n & $p} { if {$s_length == 1} { @@ -4612,7 +4562,7 @@ namespace eval punk { set start $i set end $i } else { - incr end + incr end lappend sequences [list $start $end] set in_sequence 0 set s_length 0 @@ -4649,81 +4599,11 @@ namespace eval punk { return $output } - # - # - # relatively slow on even small sized scripts - proc arg_is_script_shaped2 {arg} { - set re {^(\s|;|\n)$} - set chars [split $arg ""] - if {[lsearch -regex $chars $re] >=0} { - return 1 - } else { - return 0 - } - } - - #exclude quoted whitespace - proc arg_is_script_shaped {arg} { - if {[tcl::string::first \n $arg] >= 0} { - return 1 - } elseif {[tcl::string::first ";" $arg] >= 0} { - return 1 - } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { - lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found - return [expr {$part2 ne ""}] - } else { - return 0 - } - } - proc _rhs_tail_split {fullrhs} { - set inq 0; set indq 0 - set equalsrhs "" - set i 0 - foreach ch [split $fullrhs ""] { - if {$inq} { - append equalsrhs $ch - if {$ch eq {'}} { - set inq 0 - } - } elseif {$indq} { - append equalsrhs $ch - if {$ch eq {"}} { - set indq 0 - } - } else { - switch -- $ch { - {'} { - set inq 1 - } - {"} { - set indq 1 - } - " " { - #whitespace outside of quoting - break - } - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} - default { - #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? - #we can't (reliably?) put \t as one of our switch keys - # - if {$ch eq "\t"} { - break - } - } - } - append equalsrhs $ch - } - incr i - } - set tail [tcl::string::range $fullrhs $i end] - return [list $equalsrhs $tail] - } # -- #consider possible tilde templating version ~= vs .= - #support ~ and ~* placeholders only. - #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* + #support ~ and ~* placeholders only. + #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* #The ~ being mapped to $data in the pipeline. #This is more readable and simpler for beginners - although it doesn't handle more advanced insertion requirements. #possibility to mix as we can already with .= and = @@ -4739,12 +4619,14 @@ namespace eval punk { #--------------------------------------------------------------------- # test if we have an initial x.=y.= or x.= y.= - + #nextail is tail for possible recursion based on first argument in the segment - set nexttail [lassign $fulltail next1] ;#tail head + #set nexttail [lassign $fulltail next1] ;#tail head + set next1 [lindex $args 0] switch -- $next1 { pipematch { + set nexttail [lrange $args 1 end] set results [uplevel 1 [list pipematch {*}$nexttail]] debug.punk.pipe {>>> pipematch results: $results} 1 @@ -4773,9 +4655,9 @@ namespace eval punk { #The second element is always treated as a raw value - not a pipeline instruction. #whereas... for execution: #.= x=y the second element is a pipeline-significant symbol based on the '=' even if it was passed in as an argument. - #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - + #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - #- but if the pipeline is designed to put an argument in the zero position - then presumably it is intended as a pipeline-significant element anyway - #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines + #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines # if {$segment_op ne "="} { #handle for example: @@ -4784,7 +4666,8 @@ namespace eval punk { #var1 will contain ETC (from entire pipeline), var2 will contain etc (from associated segment) # - if {([set nexteposn [string first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + if {([set nexteposn [string last = $next1]] >= 0) && (![punk::pipe::lib::arg_is_script_shaped $next1]) } { + set nexttail [lrange $args 1 end] #*SUB* pipeline recursion. #puts "======> recurse based on next1:$next1 " if {[string index $next1 $nexteposn-1] eq {.}} { @@ -4794,7 +4677,7 @@ namespace eval punk { #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5 #debug.punk.pipe {>>> results: $results} 1 return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]] - } + } #puts "======> recurse assign based on next1:$next1 " #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #} @@ -4819,17 +4702,17 @@ namespace eval punk { set more_pipe_segments 1 ;#first loop #this contains the main %data% and %datalist% values going forward in the pipeline - #as well as any extra pipeline vars defined in each |> + #as well as any extra pipeline vars defined in each |> #It also contains any 'args' with names supplied in <| set dict_tagval [dict create] ;#cumulative %x% tag dict which operates on the whole length of the pipeline #determine if there are input args at the end of the pipeline indicated by reverse <| symbol possibly with argspecs e.g transform x y z =0} { - set tailremaining [lrange $fulltail 0 $firstargpipe_posn-1] - set argslist [lrange $fulltail $firstargpipe_posn+1 end] ;#Note that this could be a whole other pipeline with |> and/or <| elements. - set argpipe [lindex $fulltail $firstargpipe_posn] - set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " and/or <| elements. + set argpipe [lindex $fulltail $firstargpipe_posn] + set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " 0}] #if {$segment_has_insertions} { # puts stdout ">>> $segment_members insertion_patterns $insertion_patterns" @@ -4994,7 +4877,7 @@ namespace eval punk { foreach {vname val} $pipedvars { #add additionally specified vars and allow overriding of %args% and %data% by not setting them here if {$vname eq "data"} { - #already potentially overridden + #already potentially overridden continue } dict set dict_tagval $vname $val @@ -5010,7 +4893,7 @@ namespace eval punk { #add previous_result as data in end position by default, only if *no* insertions specified (data is just list-wrapped previous_result) #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default as single element - not args - because some strings are not valid lists #insertion-specs with a trailing * can be used to insert data in args format - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { lappend segment_members_filled [dict get $dict_tagval data] } @@ -5020,7 +4903,7 @@ namespace eval punk { set segment_members_filled [list] set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign - set rhsmapped [pipecmd_namemapping $rhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $rhs] set cmdname "::punk::pipecmds::insertion::_$rhsmapped" #glob chars have been mapped - so we can test by comparing info commands result to empty string if {[info commands $cmdname] eq ""} { @@ -5057,13 +4940,14 @@ namespace eval punk { } if {[dict exists $dict_tagval $v]} { set insertion_data [dict get $dict_tagval $v] - #todo - use destructure_func + #todo - use destructure_func set d [punk::_multi_bind_result $indexspec $insertion_data] set insertion_data [punk::_handle_bind_result $d] } else { #review - skip error if varname is 'data' ? #e.g we shouldn't really fail for: #.=>* list a b c <| + #??? Technically #we need to be careful not to insert empty-list as an argument by default error "pipevariable - varname $v not present in pipeline context. pipecontext_vars: [dict keys $dict_tagval] (2)" " pipecontext_vars: [dict keys $dict_tagval]" [list pipevariable variable_not_in_pipeline_scope] } @@ -5098,9 +4982,9 @@ namespace eval punk { #set segment_members_filled $segmenttail #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) - + } - set rhs [string map $dict_tagval $rhs] ;#obsolete? + set rhs [string map $dict_tagval $rhs] ;#obsolete? debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 @@ -5109,8 +4993,8 @@ namespace eval punk { #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) if {(!$segment_first_is_script ) && $segment_op eq ".="} { - #no scriptiness detected - + #no scriptiness detected + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 set cmdlist_result [uplevel 1 $segment_members_filled] @@ -5119,25 +5003,25 @@ namespace eval punk { #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] - + set segment_result [_handle_bind_result $d] #puts stderr ">>forward_result: $forward_result segment_result $segment_result" } elseif {$segment_op eq "="} { - #slightly different semantics for assigment! - #We index into the DATA - not the position within the segment! + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! #(an = segment must take a single argument, as opposed to a .= segment) #(This was a deliberate design choice for consistency with set, and to reduce errors.) #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) # - #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data - #v= {a b c} |> = + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = # must return: {a b c} not a b c # if {!$segment_has_insertions} { - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { if {![llength $segment_members_filled]} { set segment_members_filled [dict get $dict_tagval data] @@ -5168,7 +5052,7 @@ namespace eval punk { lappend segmentargnames $k lappend segmentargvals $val } - + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" set add_argsdata 0 @@ -5255,7 +5139,7 @@ namespace eval punk { #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section #It may however make a good debug point #puts stderr "segment $i segment_result:$segment_result" - + debug.punk.pipe.rep {[rep_listname segment_result]} 3 @@ -5265,17 +5149,17 @@ namespace eval punk { #examine tailremaining. # either x x x |?> y y y ... # or just y y y - #we want the x side for next loop - + #we want the x side for next loop + #set up the conditions for the next loop - #|> x=y args + #|> x=y args # inpipespec - contents of previous piper |xxx> # outpipespec - empty or content of subsequent piper |xxx> - # previous_result + # previous_result # assignment (x=y) - set pipespec($j,in) $pipespec($i,out) + set pipespec($j,in) $pipespec($i,out) set outpipespec "" set tailmap "" set next_pipe_posn -1 @@ -5295,7 +5179,7 @@ namespace eval punk { if {[llength $tailremaining] || $next_pipe_posn >= 0} { if {$next_pipe_posn >=0} { - set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] } else { @@ -5311,7 +5195,7 @@ namespace eval punk { set rhs "" set segment_first_is_script 0 if {[llength $next_all_members]} { - if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + if {[punk::pipe::lib::arg_is_script_shaped [lindex $next_all_members 0]]} { set segment_first_word [lindex $next_all_members 0] set segment_first_is_script 1 set segment_op "" @@ -5322,7 +5206,7 @@ namespace eval punk { if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op ".=" set segment_first_word [lindex $next_all_members 1] - set script_like_first_word [arg_is_script_shaped $segment_first_word] + set script_like_first_word [punk::pipe::lib::arg_is_script_shaped $segment_first_word] if {$script_like_first_word} { set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= } @@ -5330,7 +5214,7 @@ namespace eval punk { } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op "=" #never scripts - #must be at most a single element after the = ! + #must be at most a single element after the = ! if {[llength $next_all_members] > 2} { #raise this as pipesyntax as opposed to pipedata? error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] @@ -5341,7 +5225,7 @@ namespace eval punk { } else { set segment_is_list 1 ;#only used for segment_op = } - + set segment_members $segment_first_word } else { #no assignment operator and not script shaped @@ -5357,7 +5241,7 @@ namespace eval punk { } else { #?? two pipes in a row ? - debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 set segment_members return set segment_first_word return } @@ -5369,7 +5253,7 @@ namespace eval punk { } else { debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 #output pipe spec at tail of pipeline - + set pipedvars [dict create] if {[string length $pipespec($i,out)]} { set d [apply {{mv res} { @@ -5382,7 +5266,7 @@ namespace eval punk { set more_pipe_segments 0 } - #the segment_result is based on the leftmost var on the lhs of the .= + #the segment_result is based on the leftmost var on the lhs of the .= #whereas forward_result is always the entire output of the segment #JMN2 #lappend segment_result_list [join $segment_result] @@ -5414,7 +5298,7 @@ namespace eval punk { } set s $posn } else { - #int + #int if {($start < 0) || ($start > ($datalen -1))} { return 0 } @@ -5430,7 +5314,7 @@ namespace eval punk { } set e $posn } else { - #int + #int if {($end < 0)} { return 0 } @@ -5448,7 +5332,7 @@ namespace eval punk { if {$e < $s} { return 0 } - + return [expr {$e - $s + 1}] } @@ -5601,11 +5485,11 @@ namespace eval punk { #windows experiment todo - use twapi and named pipes - #twapi::namedpipe_server {\\.\pipe\something} + #twapi::namedpipe_server {\\.\pipe\something} #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc # - + if {[string first " " $new] > 0} { set c1 $name } else { @@ -5619,8 +5503,8 @@ namespace eval punk { #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] - - if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + + if {[punk::config::configure running auto_exec_mechanism] eq "experimental"} { #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it #not a trivial task @@ -5628,16 +5512,16 @@ namespace eval punk { #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output #ctrl-c propagation also needs to be considered - set teehandle punksh + set teehandle punksh uplevel 1 [list ::catch \ [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ ::tcl::UnknownResult ::tcl::UnknownOptions] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { - dict set ::tcl::UnknownOptions -code error + dict set ::tcl::UnknownOptions -code error set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" } else { - #no point returning "exitcode 0" if that's the only non-error return. + #no point returning "exitcode 0" if that's the only non-error return. #It is misleading. Better to return empty string. set ::tcl::UnknownResult "" } @@ -5647,10 +5531,10 @@ namespace eval punk { set redir ">&@stdout <@stdin" uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] - #we can't detect stdout/stderr output from the exec - #for now emit an extra \n on stderr + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr #todo - there is probably no way around this but to somehow exec in the context of a completely separate console - #This is probably a tricky problem - especially to do cross-platform + #This is probably a tricky problem - especially to do cross-platform # # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit if {[dict get $::tcl::UnknownOptions -code] == 0} { @@ -5747,7 +5631,7 @@ namespace eval punk { } } - + } return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" @@ -5756,11 +5640,12 @@ namespace eval punk { proc know {cond body} { set existing [info body ::unknown] #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) - ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##This means we can't have 2 different conds with same body if we test for body in unknown. ##if {$body ni $existing} { - package require base64 set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + + #tclint-disable-next-line proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { #--------------------------------------- if {![catch {expr {@c@}} res] && $res} { @@ -5779,7 +5664,6 @@ namespace eval punk { } proc decodescript {b64} { if {[ catch { - package require base64 base64::decode $b64 } scr]} { return "" @@ -5817,36 +5701,36 @@ namespace eval punk { if {[info commands ::tsv::set] eq ""} { puts stderr "set_repl_last_unknown - tsv unavailable!" return - } + } tsv::set repl last_unknown {*}$args } # --------------------------- + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- proc configure_unknown {} { #----------------------------- #these are critical e.g core behaviour or important for repl displaying output correctly - - #---------------- - #for var="val {a b c}" - #proc ::punk::val {{v {}}} {tailcall lindex $v} - #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version - proc ::punk::val [list [list v [purelist]]] {return $v} - #---------------- + #can't use know - because we don't want to return before original unknown body is called. proc ::unknown {args} [string cat { - package require base64 #set ::punk::last_run_display [list] #set ::repl::last_unknown [lindex $args 0] ;#jn #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW - punk::set_repl_last_unknown [lindex $args 0] + punk::set_repl_last_unknown [lindex $args 0] }][info body ::unknown] #handle process return dict of form {exitcode num etc blah} #ie when the return result as a whole is treated as a command - #exitcode must be the first key + #exitcode must be the first key know {[lindex $args 0 0] eq "exitcode"} { uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] } @@ -5854,13 +5738,13 @@ namespace eval punk { #----------------------------- # - # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. - + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + #todo - repl output info that it was evaluated as an expression #know {[expr $args] || 1} {expr $args} know {[expr $args] || 1} {tailcall expr $args} - #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} @@ -5879,18 +5763,18 @@ namespace eval punk { error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs tail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs tail } #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah # we only look at leftmost namespace-like thing and need to take account of the pattern syntax - # e.g for ::etc,'::x'= + # e.g for ::etc,'::x'= # the ns is :: and the tail is etc,'::x'= # (Tcl's namespace qualifiers/tail won't help here) if {[string match ::* $hd]} { - set patterns [punk::_split_patterns_memoized $hd] + set patterns [punk::pipe::lib::_split_patterns_memoized $hd] #get a pair-list something like: {::x /0} {etc {}} set ns [namespace qualifiers [lindex $patterns 0 0]] set nslen [string length $ns] @@ -5904,27 +5788,27 @@ namespace eval punk { } else { set nscaller [uplevel 1 [list ::namespace current]] #jmn - set rhsmapped [pipecmd_namemapping $equalsrhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $equalsrhs] set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk #we must check for exact match of the command in the list - because command could have glob chars. if {"$pattern=$rhsmapped" in $commands} { puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" #we call the namespaced function - we don't evaluate it *in* the namespace. #REVIEW - #warn for now...? + #warn for now...? #tailcall $pattern=$equalsrhs {*}$args tailcall $pattern=$rhsmapped {*}$tail } } #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" - #ignore the namespace.. + #ignore the namespace.. #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] } - #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list #e.g x=a\nb c @@ -5992,12 +5876,12 @@ namespace eval punk { error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs argstail } #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail @@ -6018,8 +5902,8 @@ namespace eval punk { know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} #add escaping backslashes to a value - #matching odd keys in dicts using pipeline syntax can be tricky - as - #e.g + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g #set ktest {a"b} #@@[escv $ktest].= list a"b val #without escv: @@ -6033,14 +5917,14 @@ namespace eval punk { #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically #thanks to DKF regsub -all {\W} $v {\\&} - } + } interp alias {} escv {} punk::escv #review #set v "\u2767" # #escv $v #\ - #the + #the #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { @@ -6048,17 +5932,17 @@ namespace eval punk { # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! # #avoid using the return from expr and it works: # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } - # + # # tailcall ::punk::match_exec $varspecs $rhs {*}$tail # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] #} } - configure_unknown + configure_unknown #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. # - #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. proc % {args} { set arglist [lassign $args assign] ;#tail, head @@ -6068,12 +5952,12 @@ namespace eval punk { tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] } - set is_script [punk::arg_is_script_shaped $assign] + set is_script [punk::pipe::lib::arg_is_script_shaped $assign] if {!$is_script && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6092,7 +5976,7 @@ namespace eval punk { tailcall {*}$cmdlist - #result-based mismatch detection can probably never work nicely.. + #result-based mismatch detection can probably never work nicely.. #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! # set result [uplevel 1 $cmdlist] @@ -6128,10 +6012,10 @@ namespace eval punk { set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] } elseif {$assign eq "="} { set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} # set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} # set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6143,10 +6027,10 @@ namespace eval punk { } } else { set cmdlist $args - #script? + #script? #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 $cmdlist} result erroptions]} { #puts stderr "pipematch erroptions:$erroptions" #debug.punk.pipe {pipematch error $result} 4 @@ -6236,7 +6120,7 @@ namespace eval punk { } } - #should only raise an error for pipe syntax errors - all other errors should be wrapped + #should only raise an error for pipe syntax errors - all other errors should be wrapped proc pipecase {args} { #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 set arglist [lassign $args assign] @@ -6245,10 +6129,10 @@ namespace eval punk { } elseif {$assign eq "="} { #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] set cmdlist [list ::= {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { @@ -6257,15 +6141,15 @@ namespace eval punk { set cmdlist [list $assign {*}$arglist] #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] } else { - error "pipesyntax pipecase unable to interpret pipeline '$args'" + error "pipesyntax pipecase unable to interpret pipeline '$args'" } #todo - account for insertion-specs e.g x=* x.=/0* } else { - #script? + #script? set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { #puts stderr "====>>> result: $result erroptions" set ecode [dict get $erroptions -errorcode] @@ -6308,14 +6192,14 @@ namespace eval punk { return [dict create error [dict create suppressed $result]] } default { - #normal tcl error + #normal tcl error #return [dict create error [dict create reason $result]] tailcall error $result "pipecase $args" [list caseerror] } } } } else { - tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] } } @@ -6329,7 +6213,7 @@ namespace eval punk { #unset args #upvar args upargs #set upargs $nextargs - upvar switchargs switchargs + upvar switchargs switchargs set switchargs $args uplevel 1 [::list ::if 1 $pipescript] } @@ -6339,7 +6223,7 @@ namespace eval punk { proc pipeswitchc {pipescript args} { set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list info vars] } else { set get_vars [list info locals] @@ -6377,13 +6261,13 @@ namespace eval punk { % - pipematch - ispipematch { incr i set e2 [lindex $args $i] - #set body [list $e {*}$e2] + #set body [list $e {*}$e2] #append body { $data} - - set body [list $e {*}$e2] + + set body [list $e {*}$e2] append body { {*}$data} - - + + set applylist [list {data} $body] #puts stderr $applylist set r [apply $applylist $r] @@ -6393,7 +6277,7 @@ namespace eval punk { incr i set e2 [lindex $args $i] set body [list $e $e2] - #pipeswitch takes 'args' - so expand $data when in pipedata context + #pipeswitch takes 'args' - so expand $data when in pipedata context append body { {*}$data} #use applylist instead of uplevel when in pipedata context! #can use either switchdata/data but not vars in calling context of 'pipedata' command. @@ -6421,8 +6305,7 @@ namespace eval punk { proc scriptlibpath {{shortname {}} args} { - upvar ::punk::config::running running_config - set scriptlib [dict get $running_config scriptlib] + set scriptlib [punk::config::configure running scriptlib] if {[string match "lib::*" $shortname]} { set relpath [string map [list "lib::" "" "::" "/"] $shortname] set relpath [string trimleft $relpath "/"] @@ -6452,7 +6335,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { set sep ";" } else { - # : ok for linux/bsd ... mac? + # : ok for linux/bsd ... mac? set sep ":" } set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] @@ -6465,7 +6348,7 @@ namespace eval punk { } proc path {{glob *}} { set pipe [punk::path_list_pipe $glob] - {*}$pipe |> list_as_lines + {*}$pipe |> list_as_lines } #------------------------------------------------------------------- @@ -6508,7 +6391,7 @@ namespace eval punk { #e.g unix files such as /dev/null vs windows devices such as CON,PRN #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! - #We will stick with the Tcl view of the file system. + #We will stick with the Tcl view of the file system. #User can use their own direct calls to external utils if #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] proc sh_TEST {args} { @@ -6526,7 +6409,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { #e.g trailing dot or trailing space if {[punk::winpath::illegalname_test $a2]} { - #protect with \\?\ to stop windows api from parsing + #protect with \\?\ to stop windows api from parsing #will do nothing if already prefixed with \\?\ set a2 [punk::winpath::illegalname_fix $a2] @@ -6536,7 +6419,7 @@ namespace eval punk { switch -- $a1 { -b { #dubious utility on FreeBSD, windows? - #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' #Linux apparently uses them though if{[file exists $a2]} { set boolresult [expr {[file type $a2] eq "blockSpecial"}] @@ -6545,7 +6428,7 @@ namespace eval punk { } } -c { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { set boolresult [expr {[file type $a2] eq "characterSpecial"}] } else { @@ -6559,9 +6442,9 @@ namespace eval punk { set boolresult [file exists $a2] } -f { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { - set boolresult [expr {[file type $a2] eq "file"}] + set boolresult [expr {[file type $a2] eq "file"}] } else { set boolresult false } @@ -6621,7 +6504,7 @@ namespace eval punk { } "-eq" { #test expects a possibly-large integer-like thing - #shell scripts will + #shell scripts will if {![is_sh_test_integer $a1]} { puts stderr "sh_TEST: invalid integer '$a1'" set lasterr 2 @@ -6725,7 +6608,7 @@ namespace eval punk { set exitcode [dict get $callinfo exitcode] if {[string length $errinfo]} { puts stderr "sh_TEST error in external call to 'test $args': $errinfo" - set lasterr $exitcode + set lasterr $exitcode } if {$exitcode == 0} { set boolresult true @@ -6761,7 +6644,7 @@ namespace eval punk { set c [lindex $args 0] if {[string is integer -strict $c]} { #return [expr {$c == 0}] - #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true if {$c == 0} { return true } else { @@ -6801,7 +6684,7 @@ namespace eval punk { #maint - punk::args has similar #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions - #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? #JMN #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. @@ -6857,7 +6740,7 @@ namespace eval punk { foreach {k v} $rawargs { if {![string match -* $k]} { break - } + } if {$i+1 >= [llength $rawargs]} { #no value for last flag error "bad options for $caller. No value supplied for last option $k" @@ -6957,7 +6840,7 @@ namespace eval punk { #NOT attempting to match haskell other than in overall concept. # - #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. # @@ -7046,7 +6929,7 @@ namespace eval punk { } #group_numlist ? preserve representation of numbers rather than use string comparison? - + # - group_string #.= punk::group_string "aabcccdefff" @@ -7131,7 +7014,7 @@ namespace eval punk { #review #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? #Perhaps will be solved by: Tip 550: Garbage collection for TclOO - #Theoretically this should allow tidy up of objects created within the pipeline automatically + #Theoretically this should allow tidy up of objects created within the pipeline automatically #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. proc matrix_command_from_rows {matrix_rows} { set mcmd [struct::matrix] @@ -7147,7 +7030,7 @@ namespace eval punk { set filtered_list [list] set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list ::info vars] } else { set get_vars [list ::info locals] @@ -7227,38 +7110,89 @@ namespace eval punk { return $linelist } - - #An implementation of a notoriously controversial metric. - proc LOC {args} { - set argspecs [subst { + namespace eval argdoc { + set DYN_ANTIGLOB_PATHS {${[punk::args::resolved_def -types opts ::punk::path::treefilenames -antiglob_paths]}} + punk::args::define { + @dynamic + @id -id ::punk::LOC + @cmd -name punk::LOC\ + -summary\ + "Lines Of Code counter"\ + -help\ + "LOC - lines of code. + An implementation of a notoriously controversial metric. + Returns a dict or dictionary-display containing various + counts such as: + 'loc' - total lines of code. + 'purepunctuationlines' - lines consisting soley of punctuation. + 'filecount' - number of files examined." + @opts + -return -default showdict -choices {dict showdict} -dir -default "\uFFFF" -exclude_dupfiles -default 1 -type boolean + ${$DYN_ANTIGLOB_PATHS} + -antiglob_files -default "" -type list -help\ + "Exclude if file tail matches any of these patterns" -exclude_punctlines -default 1 -type boolean + -show_largest -default 0 -type integer -help\ + "Report the top largest linecount files. + The value represents the number of files + to report on." + } " + #we could map away whitespace and use string is punct - but not as flexible? review -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } - }] - set argd [punk::args::get_dict $argspecs $args] - lassign [dict values $argd] leaders opts vals - set searchspecs [dict values $vals] + " { + @values + fileglob -type string -default * -optional 1 -multiple 1 -help\ + "glob patterns to match against the filename portion (last segment) of each + file path. e.g *.tcl *.tm" + } + } + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argd [punk::args::parse $args withid ::punk::LOC] + lassign [dict values $argd] leaders opts values received + set searchspecs [dict get $values fileglob] - # -- --- --- --- --- --- - set opt_dir [dict get $opts -dir] + # -- --- --- --- --- --- + set opt_return [dict get $opts -return] + set opt_dir [dict get $opts -dir] if {$opt_dir eq "\uFFFF"} { set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list } - # -- --- --- --- --- --- - set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] - set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars - set opt_punctchars [dict get $opts -punctchars] - # -- --- --- --- --- --- + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + set opt_largest [dict get $opts -show_largest] + set opt_antiglob_paths [dict get $opts -antiglob_paths] + set opt_antiglob_files [dict get $opts -antiglob_files] + # -- --- --- --- --- --- - set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set filepaths [punk::path::treefilenames -dir $opt_dir -antiglob_paths $opt_antiglob_paths -antiglob_files $opt_antiglob_files {*}$searchspecs] set loc 0 - set dupfileloc 0 - set seentails [list] + set dupfileloc 0 + set seentails [dict create] + set seencksums [dict create] ;#key is cksum value is list of paths + set largestloc [dict create] set dupfilecount 0 - set extensions [list] + set extensions [list] set purepunctlines 0 + set dupinfo [dict create] + set has_hashfunc [expr {![catch {package require sha1}]}] + set notes "" + if {$has_hashfunc} { + set dupfilemech sha1 + if {$opt_exclude_punctlines} { + append notes "checksums are on content stripped of whitespace lines,trailing whitespace, and pure punct lines. Does not indicate file contents equal.\n" + } else { + append notes "checksums are on content stripped of whitespace lines and trailing whitespace. Does not indicate file contents equal.\n" + } + } else { + set dupfilemech filetail + append notes "dupfilemech filetail because sha1 not loadable\n" + } foreach fpath $filepaths { set isdupfile 0 set floc 0 @@ -7267,111 +7201,318 @@ namespace eval punk { if {$ext ni $extensions} { lappend extensions $ext } + if {[catch {fcat $fpath} contents]} { + puts stderr "Error processing $fpath\n $contents" + continue + } + set lines [linelist -line {trimright} -block {trimall} $contents] if {!$opt_exclude_punctlines} { - set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + set floc [llength $lines] + set comparedlines $lines } else { - set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] set mapawaypunctuation [list] foreach p $opt_punctchars empty {} { lappend mapawaypunctuation $p $empty } + set comparedlines [list] foreach ln $lines { if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { incr floc + lappend comparedlines $ln } else { incr fpurepunctlines - } + } } } - if {[file tail $fpath] in $seentails} { - set isdupfile 1 - incr dupfilecount - incr dupfileloc $floc + if {$opt_largest > 0} { + dict set largestloc $fpath $floc + } + if {$has_hashfunc} { + set cksum [sha1::sha1 [encoding convertto utf-8 [join $comparedlines \n]]] + if {[dict exists $seencksums $cksum]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + dict lappend seencksums $cksum $fpath + } else { + dict set seencksums $cksum [list $fpath] + } + } else { + if {[dict exists $seentails [file tail $fpath]]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } } if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { incr loc $floc incr purepunctlines $fpurepunctlines } - lappend seentails [file tail $fpath] + dict lappend seentails [file tail $fpath] $fpath + #lappend seentails [file tail $fpath] + } + if {$has_hashfunc} { + dict for {cksum paths} $seencksums { + if {[llength $paths] > 1} { + dict set dupinfo checksums $cksum $paths + } + } + } + dict for {tail paths} $seentails { + if {[llength $paths] > 1} { + dict set dupinfo sametail $tail $paths + } } + if {$opt_exclude_punctlines} { - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + purepunctuationlines $purepunctlines\ + notes $notes\ + ] + } else { + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + notes $notes\ + ] + } + if {$opt_largest > 0} { + set largest_n [dict create] + set sorted [lsort -stride 2 -index 1 -decreasing -integer $largestloc] + set kidx 0 + for {set i 0} {$i < $opt_largest} {incr i} { + if {$kidx+1 > [llength $sorted]} {break} + dict set largest_n [lindex $sorted $kidx] [lindex $sorted $kidx+1] + incr kidx 2 + } + dict set result largest $largest_n + } + if {$opt_return eq "showdict"} { + return [punk::lib::showdict $result @@dupinfo/*/* !@@dupinfo] } - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + return $result } + ##dict of lists? + #a + # 1 + # 2 + #b + # 3 + # 4 + # "" + # etc + # d + # D + # "ok then" + + + ##dict of dicts + #a + # x + # 1 + # y + # 2 + #b + # x + # 11 + + ##dict of mixed + #list + # a + # b + # c + #dict + # a + # aa + # b + # bb + #val + # x + #list + # a + # b + + # each line has 1 key or value OR part of 1 key or value. ie <=1 key/val per line! + ##multiline + #key + # "multi + # line value" + # + #-------------------------------- + #a + # 1 + # 2 + + #vs + + #a + # 1 + # 2 + + #dict of list-len 2 is equiv to dict of dict with one keyval pair + #-------------------------------- + + - #!!!todo fix - linedict is unfinished and non-functioning - #linedict based on indents + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents proc linedict {args} { + puts stderr "linedict is experimental and incomplete" set data [lindex $args 0] - set opts [lrange $args 1 end] ;#todo + set opts [lrange $args 1 end] ;#todo set nlsplit [split $data \n] set rootindent -1 set stepindent -1 - #set wordlike_parts [regexp -inline -all {\S+} $lastitem] - set d [dict create] - set keys [list] - set i 1 - set firstkeyline "N/A" - set firststepline "N/A" + + #first do a partial loop through lines and work out the rootindent and stepindent. + #we could do this in the main loop - but we do it here to remove a small bit of logic from the main loop. + #review - if we ever move to streaming a linedict - we'll need to re-arrange to validating indents as we go anyway. + set linenum 0 + set firstkey_line "N/A" + set firstkey_linenum -1 + set firststep_line "N/A" + set firststep_linenum -1 + set indents_seen [dict create] foreach ln $nlsplit { + incr linenum if {![string length [string trim $ln]]} { - incr i continue } - set is_rootkey 0 + + #todo - use info complete to accept keys/values with newlines regexp {(\s*)(.*)} $ln _ space linedata - puts stderr ">>line:'$ln' [string length $space] $linedata" - set this_indent [string length $space] - if {$rootindent < 0} { - set firstkeyline $ln - set rootindent $this_indent + if {[catch {lindex $linedata 0}]} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent == $rootindent} { - set is_rootkey 1 + if {[llength $linedata] > 1} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent < $rootindent} { - error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + #puts stderr "--linenum:[format %-3s $linenum] line:[format "%-20s" $ln] [format %-4s [string length $space]] $linedata" + set this_indent [string length $space] + if {[dict exists $indents_seen $this_indent]} { + continue } - if {$is_rootkey} { - dict set d $linedata {} - lappend keys $linedata + if {$rootindent < 0} { + set firstkey_line $ln + set firstkey_linenum $linenum + set rootindent $this_indent + dict set indents_seen $this_indent 1 + } elseif {$stepindent < 0} { + if {$this_indent > $rootindent} { + set firststep_line $ln + set firststep_linenum $linenum + set stepindent [expr {$this_indent - $rootindent}] + dict set indents_seen $this_indent 1 + } elseif {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" + } + #if equal - it's just another root key } else { - if {$stepindent < 0} { - set stepindent $this_indent - set firststepline $ln + #validate all others + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" } - if {$this_indent == $stepindent} { - dict set d [lindex $keys end] $ln + if {($this_indent - $rootindent) % $stepindent != 0} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. this_indent - rootindent ($this_indent - $rootindent == [expr {$this_indent - $rootindent}]) is not a multiple of the first key indent $stepindent seen on linenumber: $firststep_linenum value:'$firststep_line'" } else { - if {($this_indent % $stepindent) != 0} { - error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" - } + dict set indents_seen $this_indent 1 + } + } + } + - #todo fix! + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set linenum 0 ;#line-numbers 1 based + foreach ln $nlsplit { + incr linenum + if {![string length [string trim $ln]]} { + incr linenum + continue + } + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>linenum:[format %-3s $linenum] line:[format "%-20s " $ln] [format %-4s [string length $space]] $linedata" + set linedata [lindex $linedata 0] + set this_indent [string length $space] + + + if {$this_indent == $rootindent} { + #is rootkey + dict set d $linedata {} + set keys [list $linedata] + } else { + set ispan [expr {$this_indent - $rootindent}] + set numsteps [expr {$ispan / $stepindent}] + #assert - since validated in initial loop - numsteps is always >= 1 + set keydepth [llength $keys] + if {$numsteps > $keydepth + 1} { + #too deep - not tested for in initial loop. ? todo - convert to leading spaces in key/val? + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + if {$numsteps > ($keydepth - 1)} { + #assert - from above test - must be 1 or 2 deeper set parentkey [lindex $keys end] - lappend keys [list $parentkey $ln] - set oldval [dict get $d $parentkey] - if {[string length $oldval]} { - set new [dict create $oldval $ln] + set oldval [dict get $d {*}$parentkey] + if {$numsteps - ($keydepth -1) == 1} { + #1 deeper + if {$oldval ne {}} { + lappend keys [list {*}$parentkey $linedata] + dict unset d {*}$parentkey + #dict set d {*}$parentkey $oldval $linedata + dict set d {*}$parentkey $oldval {} ;#convert to key? + dict set d {*}$parentkey $linedata {} + } else { + dict set d {*}$parentkey $linedata + } } else { - dict set d $parentkey $ln - } - + #2 deeper - only ok if there is an existing val + if {$oldval eq {}} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + puts ">>> 2deep d:'$d' oldval:$oldval linedata:$linedata parentkey:$parentkey" + dict unset d {*}$parentkey + dict set d {*}$parentkey $oldval $linedata + lappend keys [list {*}$parentkey $oldval] + } + } elseif {$numsteps < ($keydepth - 1)} { + set diff [expr {$keydepth - 1 - $numsteps}] + set keys [lrange $keys 0 end-$diff] + #now treat as same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} + } else { + #same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} } } - incr i + #puts ">>keys:$keys" } return $d } - proc dictline {d} { + proc dictline {d {indent 2}} { puts stderr "unimplemented" set lines [list] - + return $lines } @@ -7414,79 +7555,79 @@ namespace eval punk { @id -id ::punk::inspect @cmd -name punk::inspect -help\ "Function to display values - used pimarily in a punk pipeline. - The raw value arguments (not options) are always returned to pass - forward in the pipeline. - (pipeline data inserted at end of each |...> segment is passed as single item unless - inserted with an expanding insertion specifier such as .=>* ) - e.g1: - .= list a b c |v1,/1-end,/0>\\ - .=>* inspect -label i1 -- |>\\ - .=v1> inspect -label i2 -- |>\\ - string toupper - (3) i1: {a b c} {b c} a - (1) i2: a b c - - - A B C - " + The raw value arguments (not options) are always returned to pass + forward in the pipeline. + (pipeline data inserted at end of each |...> segment is passed as single item unless + inserted with an expanding insertion specifier such as .=>* ) + e.g1: + .= list a b c |v1,/1-end,/0>\\ + .=>* inspect -label i1 -- |>\\ + .=v1> inspect -label i2 -- |>\\ + string toupper + (3) i1: {a b c} {b c} a + (1) i2: a b c + + - A B C + " -label -type string -default "" -help\ "An optional label to help distinguish output when multiple - inspect statements are in a pipeline. This appears after the - bracketed count indicating number of values supplied. - e.g (2) MYLABEL: val1 val2 - The label can include ANSI codes. - e.g - inspect -label [a+ red]mylabel -- val1 val2 val3 - " + inspect statements are in a pipeline. This appears after the + bracketed count indicating number of values supplied. + e.g (2) MYLABEL: val1 val2 + The label can include ANSI codes. + e.g + inspect -label [a+ red]mylabel -- val1 val2 val3 + " -limit -type int -default 20 -help\ "When multiple values are passed to inspect - limit the number - of elements displayed in -channel output. - When truncation has occured an elipsis indication (...) will be appended. - e.g - .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ - (11) 20 23 26 29... + of elements displayed in -channel output. + When truncation has occured an elipsis indication (...) will be appended. + e.g + .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ + (11) 20 23 26 29... - - 385 + - 385 - For no limit - use -limit -1 - " + For no limit - use -limit -1 + " -channel -type string -default stderr -help\ "An existing open channel to write to. If value is any of nul, null, /dev/nul - the channel output is disabled. This effectively disables inspect as the args - are simply passed through in the return to continue the pipeline. - " + the channel output is disabled. This effectively disables inspect as the args + are simply passed through in the return to continue the pipeline. + " -showcount -type boolean -default 1 -help\ "Display a leading indicator in brackets showing the number of arg values present." -ansi -type integer -default 1 -nocase 1 -choices {0 1 2 VIEW 3 VIEWCODES 4 VIEWSTYLES} -choicelabels { 0 "Strip ANSI codes from display - of values. The disply output will - still be colourised if -ansibase has - not been set to empty string or - [a+ normal]. The stderr or stdout - channels may also have an ansi colour. - (see 'colour off' or punk::config)" + of values. The disply output will + still be colourised if -ansibase has + not been set to empty string or + [a+ normal]. The stderr or stdout + channels may also have an ansi colour. + (see 'colour off' or punk::config)" 1 "Leave value as is" 2 "Display the ANSI codes and - other control characters inline - with replacement indicators. - e.g esc, newline, space, tab" + other control characters inline + with replacement indicators. + e.g esc, newline, space, tab" VIEW "Alias for 2" - 3 "Display as per 2 but with - colourised ANSI replacement codes." + 3 "Display as per 2 but with + colourised ANSI replacement codes." VIEWCODES "Alias for 3" 4 "Display ANSI and control - chars in default colour, but - apply the contained ansi to - the text portions so they display - as they would for -ansi 1" - VIEWSTYLE "Alias for 4" - } + chars in default colour, but + apply the contained ansi to + the text portions so they display + as they would for -ansi 1" + VIEWSTYLE "Alias for 4" + } -ansibase -type ansistring -default {${[a+ brightgreen]}} -help\ "Base ansi code(s) that will apply to output written to the chosen -channel. - If there are ansi resets in the displayed values - output will revert to this base. - Does not affect return value." + If there are ansi resets in the displayed values - output will revert to this base. + Does not affect return value." -- -type none -help\ "End of options marker. - It is advisable to use this, as data in a pipeline may often begin with -" + It is advisable to use this, as data in a pipeline may often begin with -" @values -min 0 -max -1 arg -type string -optional 1 -multiple 1 -help\ @@ -7500,7 +7641,7 @@ namespace eval punk { set flags [list] set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- if {$endoptsposn >= 0} { - set flags [lrange $args 0 $endoptsposn-1] + set flags [lrange $args 0 $endoptsposn-1] set pipeargs [lrange $args $endoptsposn+1 end] } else { #no explicit end of opts marker @@ -7551,7 +7692,7 @@ namespace eval punk { set val [lindex $pipeargs 0] set count 1 } else { - #but the pipeline segment could have an insertion-pattern ending in * + #but the pipeline segment could have an insertion-pattern ending in * set val $pipeargs set count [llength $pipeargs] } @@ -7597,7 +7738,7 @@ namespace eval punk { set ansibase [dict get $opts -ansibase] if {$ansibase ne ""} { - #-ansibase default is hardcoded into punk::args definition + #-ansibase default is hardcoded into punk::args definition #run a test using any ansi code to see if colour is still enabled if {[a+ red] eq ""} { set ansibase "" ;#colour seems to be disabled @@ -7609,27 +7750,31 @@ namespace eval punk { set displayval $ansibase[punk::ansi::ansistrip $displayval] } 1 { - #val may have ansi - including resets. Pass through ansibase_lines to + #val may have ansi - including resets. Pass through ansibase_lines to if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 2 { set displayval $ansibase[ansistring VIEW $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 3 { set displayval $ansibase[ansistring VIEWCODE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 4 { set displayval $ansibase[ansistring VIEWSTYLE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } } @@ -7665,6 +7810,7 @@ namespace eval punk { set cmdinfo [list] lappend cmdinfo [list help "?topics?" "This help. To see available subitems type: help topics"] lappend cmdinfo [list i "cmd ?subcommand...?" "Show usage for a command or ensemble subcommand"] + lappend cmdinfo [list s "cmd ?subcommand...?" "Show synopsis for a command or ensemble subcommand"] lappend cmdinfo [list ./ "?subdir?" "view/change directory"] lappend cmdinfo [list ../ "" "go up one directory"] lappend cmdinfo [list ./new "subdir" "make new directory and switch to it"] @@ -7692,9 +7838,9 @@ namespace eval punk { $t configure_column 1 -minwidth [expr {$width_1 + 1}] $t configure -title $title - set text "" + set text "" append text [$t print] - + set warningblock "" set introblock $mascotblock @@ -7743,14 +7889,14 @@ namespace eval punk { upvar ::punk::config::other_env_vars_config otherenv_config set known_punk [dict keys $punkenv_config] - set known_other [dict keys $otherenv_config] + set known_other [dict keys $otherenv_config] append text \n set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. - #The Tcl ::env array is linked to the underlying process view of the environment + #The Tcl ::env array is linked to the underlying process view of the environment #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. #an 'array get' will resynchronise. #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. @@ -7759,7 +7905,7 @@ namespace eval punk { #do an array read on ::env foreach {v vinfo} $punkenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7778,7 +7924,7 @@ namespace eval punk { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7795,12 +7941,12 @@ namespace eval punk { append text $linesep\n append text "punk environment vars:\n" append text $linesep\n - set col1 [string repeat " " 25] + set col1 [string repeat " " 25] set col2 [string repeat " " 50] foreach v $known_punk { set c1 [overtype::left $col1 $v] if {[info exists ::env($v)]} { - set c2 [overtype::left $col2 [set ::env($v)] + set c2 [overtype::left $col2 [set ::env($v)]] } else { set c2 [overtype::right $col2 "(NOT SET)"] } @@ -7816,27 +7962,33 @@ namespace eval punk { set indent [string repeat " " [string length "WARNING: "]] lappend cstring_tests [dict create\ type "PM "\ - msg "PRIVACY MESSAGE"\ + msg "UN"\ f7 punk::ansi::controlstring_PM\ - f7desc "7bit ESC ^"\ + f7prefix "7bit ESC ^ secret "\ + f7suffix "safe"\ f8 punk::ansi::controlstring_PM8\ - f8desc "8bit \\x9e"\ + f8prefix "8bit \\x9e secret "\ + f8suffix "safe"\ ] lappend cstring_tests [dict create\ type SOS\ - msg "STRING"\ + msg "NOT"\ f7 punk::ansi::controlstring_SOS\ - f7desc "7bit ESC X"\ + f7prefix "7bit ESC X string "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_SOS8\ - f8desc "8bit \\x98"\ + f8prefix "8bit \\x98 string "\ + f8suffix " hidden"\ ] lappend cstring_tests [dict create\ type APC\ - msg "APPLICATION PROGRAM COMMAND"\ + msg "NOT"\ f7 punk::ansi::controlstring_APC\ - f7desc "7bit ESC _"\ + f7prefix "7bit ESC _ APPLICATION PROGRAM COMMAND "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_APC8\ - f8desc "8bit \\x9f"\ + f8prefix "8bit \\x9f APPLICATION PROGRAM COMMAND "\ + f8suffix " hidden"\ ] foreach test $cstring_tests { @@ -7846,14 +7998,14 @@ namespace eval punk { set hidden_width_m8 [punk::console::test_char_width $m8] if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { if {$hidden_width_m == 0} { - set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + set d "[a+ green bold][dict get $test f7prefix][a red]${m}[a][a+ green bold][dict get $test f7suffix][a]" } else { - set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + set d "[a+ yellow bold][dict get $test f7prefix][a red]$m[a][a+ yellow bold][dict get $test f7suffix][a]" } if {$hidden_width_m8 == 0} { - set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ green ][dict get $test f8prefix][a red]$m8[a][a+ green][dict get $test f8suffix][a]" } else { - set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ yellow bold][dict get $test f8prefix][a red]$m8[a][a+ yellow bold][dict get $test f8suffix][a]" } append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" } @@ -7923,7 +8075,7 @@ namespace eval punk { } set widest0 [$t column_datawidth 0] $t configure_column 0 -minwidth [expr {$widest0 + 4}] - append text \n[$t print] + append text \n[$t print] lappend chunks [list stdout $text] } @@ -7933,7 +8085,7 @@ namespace eval punk { proc help {args} { set chunks [help_chunks {*}$args] foreach chunk $chunks { - lassign $chunk chan text + lassign $chunk chan text puts -nonewline $chan $text } } @@ -7963,8 +8115,7 @@ namespace eval punk { interp alias {} know {} punk::know interp alias {} know? {} punk::know? - #interp alias {} arg {} punk::val - interp alias {} val {} punk::val + #interp alias {} val {} punk::val interp alias {} exitcode {} punk::exitcode interp alias {} hide {} punkapp::hide_console ;#will only work if controllable toplevels exist @@ -7979,7 +8130,7 @@ namespace eval punk { - + #friendly sh aliases (which user may wish to disable e.g if conflicts) interp alias {} test {} punk::sh_test ;#not much reason to run 'test' directly in punk shell (or tclsh shell) as returncode not obvious anyway due to use of exec interp alias {} TEST {} punk::sh_TEST; #double-evaluation to return tcl true/false from exitcode @@ -8016,7 +8167,7 @@ namespace eval punk { #---------------------------------------------- interp alias {} linelistraw {} punk::linelistraw - + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? interp alias {} PATH {} punk::path @@ -8066,13 +8217,13 @@ namespace eval punk { # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion interp alias {} l {} sh_runout -n ls -A ;#plain text listing - #interp alias {} ls {} sh_runout -n ls -AF --color=always + #interp alias {} ls {} sh_runout -n ls -AF --color=always interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less #note that shell globbing with * won't work on unix systems when using unknown/exec interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? - #interp alias {} lw {} ls -aFv --color=always + #interp alias {} lw {} ls -aFv --color=always interp alias {} dir {} shellrun::runconsole dir @@ -8093,7 +8244,7 @@ namespace eval punk { interp alias {} ./~ {} punk::nav::fs::d/~ interp alias {} d/~ {} punk::nav::fs::d/~ interp alias "" x/ "" punk::nav::fs::x/ - + if {$::tcl_platform(platform) eq "windows"} { set has_powershell 1 @@ -8101,10 +8252,10 @@ namespace eval punk { interp alias {} dw {} dir /W/D } else { #todo - natsorted equivalent - #interp alias {} dl {} + #interp alias {} dl {} interp alias {} dl {} puts stderr "not implemented" interp alias {} dw {} puts stderr "not implemented" - #todo - powershell detection on other platforms + #todo - powershell detection on other platforms set has_powershell 0 } if {$has_powershell} { @@ -8142,7 +8293,7 @@ namespace eval punk { if {[punk::repl::codethread::is_running]} { puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" set ::repl::done 1 - } + } } start { if {[punk::repl::codethread::is_running]} { @@ -8167,8 +8318,8 @@ punk::mod::cli set_alias app #todo - change to punk::dev package require punk::mix -punk::mix::cli set_alias dev -punk::mix::cli set_alias deck ;#deprecate! +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! #todo - add punk::deck for managing cli modules and commandsets diff --git a/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/bootsupport/modules/punk/aliascore-0.1.0.tm index fd638812..b8fada0b 100644 --- a/src/bootsupport/modules/punk/aliascore-0.1.0.tm +++ b/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -118,6 +118,7 @@ tcl::namespace::eval punk::aliascore { pdict ::punk::lib::pdict\ plist {::punk::lib::pdict -roottype list}\ showlist {::punk::lib::showdict -roottype list}\ + grepstr ::punk::grepstr\ rehash ::punk::rehash\ showdict ::punk::lib::showdict\ ansistrip ::punk::ansi::ansistrip\ @@ -136,6 +137,7 @@ tcl::namespace::eval punk::aliascore { rmcup ::punk::console::disable_alt_screen\ config ::punk::config\ s ::punk::ns::synopsis\ + eg ::punk::ns::eg\ ] #*** !doctools diff --git a/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/bootsupport/modules/punk/ansi-0.1.1.tm index b8d172da..6b04827d 100644 --- a/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -611,7 +611,7 @@ tcl::namespace::eval punk::ansi { } ""] proc example {args} { - set argd [punk::args::get_by_id ::punk::ansi::example $args] + set argd [punk::args::parse $args withid ::punk::ansi::example] set colwidth [dict get $argd opts -colwidth] if {[info commands file] eq ""} { error "file command unavailable - punk::ansi::example cannot be shown" @@ -723,7 +723,8 @@ tcl::namespace::eval punk::ansi { } lappend adjusted_row $i } - append result [textblock::join_basic -- {*}$adjusted_row] \n + #append result [textblock::join_basic -- {*}$adjusted_row] \n + append result [textblock::join_basic_raw {*}$adjusted_row] \n incr rowindex } @@ -876,6 +877,7 @@ tcl::namespace::eval punk::ansi { tlc l\ trc k\ blc m\ + brc j\ ltj t\ rtj u\ ttj w\ @@ -985,51 +987,51 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #css 1-2.0 HTML 3.2-4 Basic colours eg web-silver for fg Web-silver for bg # variable WEB_colour_map_basic - tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF - tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 - tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 - tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 - tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 - tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 - tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 - tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 - tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 - tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF - tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 - tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF - tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 - tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF - tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 + tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 + tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 + tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 + tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 + tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF + tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 # -- --- --- #Pink colours variable WEB_colour_map_pink - tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 - tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 - tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 - tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 - tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 - tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB + tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 + tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 + tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 + tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 + tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 + tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB # -- --- --- #Red colours variable WEB_colour_map_red - tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 - tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 - tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C - tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C - tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 - tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 - tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A - tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A + tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 + tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 + tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C + tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C + tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 + tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 + tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A + tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A # -- --- --- #Orange colours variable WEB_colour_map_orange - tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 - tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 - tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 - tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 - tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 + tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 + tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 + tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 + tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 + tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 # -- --- --- #Yellow colours variable WEB_colour_map_yellow @@ -1041,7 +1043,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_yellow palegoldenrod 238-232-170 ;# #EEE8AA tcl::dict::set WEB_colour_map_yellow moccasin 255-228-181 ;# #FFE4B5 tcl::dict::set WEB_colour_map_yellow papayawhip 255-239-213 ;# #FFEFD5 - tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 + tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 tcl::dict::set WEB_colour_map_yellow lemonchiffon 255-250-205 ;# #FFFACD tcl::dict::set WEB_colour_map_yellow lightyellow 255-255-224 ;# #FFFFE0 # -- --- --- @@ -1068,7 +1070,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Purple, violet, and magenta colours variable WEB_colour_map_purple tcl::dict::set WEB_colour_map_purple indigo 75-0-130 ;# #4B0082 - tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 tcl::dict::set WEB_colour_map_purple darkmagenta 139-0-139 ;# #8B008B tcl::dict::set WEB_colour_map_purple darkviolet 148-0-211 ;# #9400D3 tcl::dict::set WEB_colour_map_purple darkslateblue 72-61-139 ;# #9400D3 @@ -1089,10 +1091,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Blue colours variable WEB_colour_map_blue tcl::dict::set WEB_colour_map_blue midnightblue 25-25-112 ;# #191970 - tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 tcl::dict::set WEB_colour_map_blue darkblue 0-0-139 ;# #00008B tcl::dict::set WEB_colour_map_blue mediumblue 0-0-205 ;# #0000CD - tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF tcl::dict::set WEB_colour_map_blue royalblue 65-105-225 ;# #4169E1 tcl::dict::set WEB_colour_map_blue steelblue 70-130-180 ;# #4682B4 tcl::dict::set WEB_colour_map_blue dodgerblue 30-144-255 ;# #1E90FF @@ -1113,7 +1115,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_cyan darkturquoise 0-206-209 ;# #00CED1 tcl::dict::set WEB_colour_map_cyan mediumturquoise 72-209-204 ;# #48D1CC tcl::dict::set WEB_colour_map_cyan turquoise 64-224-208 ;# #40E0D0 - tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF tcl::dict::set WEB_colour_map_cyan cyan 0-255-255 ;# #00FFFF - same as aqua tcl::dict::set WEB_colour_map_cyan aquamarine 127-255-212 ;# #7FFFD4 tcl::dict::set WEB_colour_map_cyan paleturquoise 175-238-238 ;# #AFEEEE @@ -1126,11 +1128,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_green darkolivegreen 85-107-47 ;# #55682F tcl::dict::set WEB_colour_map_green forestgreen 34-139-34 ;# #228B22 tcl::dict::set WEB_colour_map_green seagreen 46-139-87 ;# #2E8B57 - tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 tcl::dict::set WEB_colour_map_green olivedrab 107-142-35 ;# #6B8E23 tcl::dict::set WEB_colour_map_green mediumseagreen 60-179-113 ;# #3CB371 tcl::dict::set WEB_colour_map_green limegreen 50-205-50 ;# #32CD32 - tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 tcl::dict::set WEB_colour_map_green springgreen 0-255-127 ;# #00FF7F tcl::dict::set WEB_colour_map_green mediumspringgreen 0-250-154 ;# #00FA9A tcl::dict::set WEB_colour_map_green darkseagreen 143-188-143 ;# #8FBC8F @@ -1160,15 +1162,15 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_white mintcream 245-255-250 ;# #F5FFFA tcl::dict::set WEB_colour_map_white snow 255-250-250 ;# #FFFAFA tcl::dict::set WEB_colour_map_white ivory 255-255-240 ;# #FFFFF0 - tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF # -- --- --- #Gray and black colours variable WEB_colour_map_gray - tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 tcl::dict::set WEB_colour_map_gray darkslategray 47-79-79 ;# #2F4F4F tcl::dict::set WEB_colour_map_gray dimgray 105-105-105 ;# #696969 tcl::dict::set WEB_colour_map_gray slategray 112-128-144 ;# #708090 - tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 tcl::dict::set WEB_colour_map_gray lightslategray 119-136-153 ;# #778899 tcl::dict::set WEB_colour_map_gray darkgray 169-169-169 ;# #A9A9A9 tcl::dict::set WEB_colour_map_gray silver 192-192-192 ;# #C0C0C0 @@ -1201,6 +1203,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set X11_colour_map [tcl::dict::merge $WEB_colour_map $X11_colour_map_diff] + + + + #Xterm colour names (256 colours) #lists on web have duplicate names #these have been renamed here in a systematic way: @@ -1217,6 +1223,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #The xterm names are boringly unimaginative - and also have some oddities such as: # DarkSlateGray1 which looks much more like cyan.. # The greyxx names are spelt with an e - but the darkslategrayX variants use an a. Perhaps that's because they are more cyan than grey and the a is a hint? + #(more likely just a mix of UK vs US spelling) # there is no gold or gold2 - but there is gold1 and gold3 #but in general the names bear some resemblance to the colours and are at least somewhat intuitive. @@ -1612,7 +1619,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set fg "black" } } - lappend clist "[a+ {*}$fc {*}$fg Term$i][format %3s $i]" + lappend clist "[a+ {*}$fc {*}$fg Term-$i][format %3s $i]" } set t [textblock::list_as_table -columns 36 -return tableobject $clist] @@ -1636,7 +1643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 8} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] } @@ -1698,7 +1705,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { set br "" } - append out "$br[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "$br[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } append out [a] return [tcl::string::trimleft $out \n] @@ -1723,7 +1730,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_seps 0 -show_edge 0 for {set i 16} {$i <=231} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== $cols} { lappend rows $row set row [list] @@ -1792,7 +1799,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu foreach cnum $pastel8 { append p8 "[a+ {*}$fc $fg Term-$cnum][format %3s $cnum] " } - append p8 [a]\n + #append p8 [a]\n + #append out \n $p8 + + append p8 [a] append out \n $p8 return $out @@ -1879,7 +1889,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 243} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] @@ -1899,7 +1909,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_hseps 0 -show_edge 0 for {set i 232} {$i <=255} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== 8} { lappend rows $row set row [list] @@ -1919,6 +1929,169 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return [tcl::string::trimleft $out \n] } + + if {[catch {package require punk::ansi::colourmap} errM]} { + puts stderr "punk::ansi FAILED to load punk::ansi::colourmap\n$errM" + } + if {[info exists ::punk::ansi::colourmap::TK_colour_map]} { + upvar ::punk::ansi::colourmap::TK_colour_map TK_colour_map + upvar ::punk::ansi::colourmap::TK_colour_map_lookup TK_colour_map_lookup + } else { + puts stderr "Failed to find TK_colour_map - punk::ansi::colourmap package not loaded?" + variable TK_colour_map {} + variable TK_colour_map_lookup {} + } + + #order-preserving + proc lunique {list} { + set new {} + foreach item $list { + if {$item ni $new} { + lappend new $item + } + } + return $new + } + + proc colourtable_tk {args} { + set opts {-forcecolour 0 -groups * -merged 0 -globs *} + foreach {k v} $args { + switch -- $k { + -groups - -merged - -forcecolour - -globs { + tcl::dict::set opts $k $v + } + default { + error "colourtable_tk unrecognised option '$k'. Known-options: [tcl::dict::keys $opts]" + } + } + } + set fc "" + if {[tcl::dict::get $opts -forcecolour]} { + set fc "forcecolour" + } + + #not implemented - todo? Tk + set groups [tcl::dict::get $opts -groups] + + set do_merge [tcl::dict::get $opts -merged] + set globs [tcl::dict::get $opts -globs] + + + + set blocklist [list] ;#vertical blocks consisting of blockrows + set blockrow [list] + set height 50 ;#number of lines (excluding header) vertically in a blockrow + set columns 5 ;#number of columns in a blockrow + variable TK_colour_map ;#use the version without lowercased additions - this gives the display names with casing as shown in Tk colour man page. + if {!$do_merge} { + set map $TK_colour_map + if {$globs eq "*"} { + set keys [dict keys $TK_colour_map] + } else { + set keys [list] + set mapkeys [dict keys $TK_colour_map] + foreach g $globs { + #lappend keys {*}[dict keys $map $g] + #need case insensitive globs for convenience. + lappend keys {*}[lsearch -all -glob -inline -nocase $mapkeys $g] + } + set keys [lunique $keys] + } + } else { + #todo - make glob fully search when do_merge + #needs to get keys from all names - but then map to keys that have dependent names + upvar ::punk::ansi::colourmap::TK_colour_map_merge map + upvar ::punk::ansi::colourmap::TK_colour_map_reverse reversemap + if {$globs eq "*"} { + set keys [dict keys $map] + } else { + set keys [list] + set allkeys [dict keys $TK_colour_map] + + foreach g $globs { + set matchedkeys [lsearch -all -glob -inline -nocase $allkeys $g] + foreach m $matchedkeys { + if {![dict exists $map $m]} { + #not a parent in a merge + set rgb [dict get $TK_colour_map $m] + set names [dict get $reversemap $rgb] + #first name is the one that is in the merge map + lappend keys [lindex $names 0] + } else { + lappend keys $m + } + } + } + set keys [lunique $keys] + } + } + set overheight 0 + + + set t "" + set start 0 + set colidx -1 + set i -1 + foreach cname $keys { + incr i + set data [dict get $map $cname] + if {$overheight || $i % $height == 0} { + set overheight 0 + incr colidx + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + $t destroy + if {$colidx % $columns == 0} { + lappend blocklist $blockrow + set blockrow [list] + } + } + set start $i + set t [textblock::class::table new] + $t configure -show_edge 0 -show_seps 0 -show_header 1 -minwidth 42 + } + if {!$do_merge} { + set cdec $data + $t add_row [list $cname " [colour_dec2hex $cdec] " $cdec] + } else { + set cdec [dict get $data colour] + set othernames [dict get $data names] + set ndisplay [join [list $cname {*}$othernames] \n] + $t add_row [list $ndisplay " [colour_dec2hex $cdec] " $cdec] + set overheight 0 + foreach n $othernames { + incr i + if {$i % $height == 0} { + set overheight 1 + } + } + } + set fg "rgb-$cdec-contrasting" + $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] + } + + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + lappend blocklist $blockrow + $t destroy + } + + set result "" + foreach blockrow $blocklist { + append result [textblock::join -- {*}$blockrow] \n + } + + return $result + } + #set WEB_colour_map [tcl::dict::merge\ # $WEB_colour_map_basic\ # $WEB_colour_map_pink\ @@ -1970,17 +2143,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } set grouptables [list] - set white_fg_list [list\ - mediumvioletred deeppink\ - darkred red firebrick crimson indianred\ - orangered\ - maroon brown saddlebrown sienna\ - indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ - midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ - teal darkcyan\ - darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ - black darkslategray dimgray slategray\ - ] + #set white_fg_list [list\ + # mediumvioletred deeppink\ + # darkred red firebrick crimson indianred\ + # orangered\ + # maroon brown saddlebrown sienna\ + # indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ + # midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ + # teal darkcyan\ + # darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ + # black darkslategray dimgray slategray\ + # ] foreach g $show_groups { #upvar WEB_colour_map_$g map_$g variable WEB_colour_map_$g @@ -1988,11 +2161,12 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t configure -show_edge 0 -show_seps 0 -show_header 1 tcl::dict::for {cname cdec} [set WEB_colour_map_$g] { $t add_row [list "$cname " "[colour_dec2hex $cdec] " $cdec] - if {$cname in $white_fg_list} { - set fg "web-white" - } else { - set fg "web-black" - } + set fg "rgb-$cdec-contrasting" + #if {$cname in $white_fg_list} { + # set fg "web-white" + #} else { + # set fg "web-black" + #} #$t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Web-$cname] } @@ -2083,12 +2257,66 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return $displaytable } + lappend PUNKARGS [list { + @id -id ::punk::ansi::a? + @cmd -name "punk::ansi::a?"\ + -summary\ + "ANSI colour information"\ + -help\ + "" + @form -form "sgr_overview" + @values -form "sgr_overview" -min 0 -max 0 + + + @form -form "term" + @leaders -form "term" -min 1 -max 1 + term -type literal(term) -help\ + "256 term colours" + @opts -min 0 -max 0 + @values -form "term" -min 0 -max -1 + panel -type string -optional 1 -multiple 1 -default {16 main greyscale note}\ + -choices {16 main greyscale pastel rainbow note} + + @form -form "tk" + @leaders -form "tk" -min 1 -max 1 + tk -type literal(tk)|literal(TK) -help\ + "Tk colours" + @opts -form "tk" + -merged -type none -help\ + "If this flag is supplied - show colour names with the same RGB + values together." + @values -form "tk" -min 0 -max -1 + glob -type string -optional 1 -multiple 1 -help\ + "A glob string such as *green*. + Multiple glob entries can be provided. The search is case insensitive" + + + @form -form "web" + @values -form "web" -min 1 -max -1 + web -type literal(web) -help\ + "Web colours" + panel -type string -optional 1 -multiple 1 -choices {basic pink red orange yellow brown purple blue cyan green white gray} + + @form -form "x11" + @values -form "x11" -min 1 -max 1 + x11 -type literal(x11) -help\ + "x11 colours" + + + @form -form "sample" + @values -form "sample" -min 1 -max -1 + colourcode -type sgr|Sgr|literalprefix(term-)|literalprefix(Term-)|literalprefix(web-)|literalprefix(Web-)|literalprefix(rgb)|literalprefix(Rgb)\ + -optional 0\ + -multiple 1 + + }] proc a? {args} { #*** !doctools #[call [fun a?] [opt {ansicode...}]] #[para]Return an ansi string representing a table of codes and a panel showing the colours variable SGR_setting_map variable SGR_colour_map + variable TK_colour_map_lookup set fcposn [lsearch $args "force*"] set fc "" set opt_forcecolour 0 @@ -2172,6 +2400,20 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu append out \n append out "[a+ {*}$fc web-white]X11 colours[a] - mostly match Web colours" \n append out [textblock::join -- $indent "To see differences: a? x11"] \n + append out \n + append out "[a+ {*}$fc web-white]Tk colours[a]" \n + append out [textblock::join -- $indent "To see all 750+ names use: a? tk"] \n + append out [textblock::join -- $indent "Restrict the results using globs e.g a? tk *green* *yellow*"] \n + append out [textblock::join -- $indent "The foreground colour in this table is generated using the contrasting suffix"] \n + append out [textblock::join -- $indent "Example: \[a+ tk-tan-contrasting Tk-tan\]text\[a] -> [a+ {*}$fc tk-tan-contrasting Tk-tan]text[a]"] \n + append out \n + append out "[a+ {*}$fc web-white]Combination testing[a]" \n + append out [textblock::join -- $indent "Example: a? red brightgreen underline Tk-slategrey italic"] \n + append out [textblock::join -- $indent "This will show a small table of each applied code and a RESULT row. The 'red' in this case is redundant,"] \n + append out [textblock::join -- $indent "so a final MERGED row displays with an alert 'REDUNDANCIES FOUND'."] \n + append out [textblock::join -- $indent "The final columns of RESULT and MERGED (showing raw ANSI sequence) will differ if the arguments aren't in canonical order."] \n + append out [textblock::join -- $indent "The MERGED line will only display if there are redundancies or different ordering."] \n + if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { append out \n @@ -2191,40 +2433,74 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { switch -- [lindex $args 0] { term { - set termargs [lrange $args 1 end] - foreach ta $termargs { - switch -- $ta { - pastel - rainbow {} - default {error "unrecognised term option '$ta'. Known values: pastel rainbow"} - } - } - set out "16 basic colours\n" - append out [colourtable_16_names -forcecolour $opt_forcecolour] \n - append out "216 colours\n" - append out [colourtable_216_names -forcecolour $opt_forcecolour] \n - append out "24 greyscale colours\n" - append out [colourtable_24_names -forcecolour $opt_forcecolour] - foreach ta $termargs { - switch -- $ta { + set argd [punk::args::parse $args -form "term" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + set panels [dict get $values panel] + + set out "" + foreach panel $panels { + #punk::args has already resolved prefixes to full panel names + switch -- $panel { + 16 { + append out "16 basic colours\n" + append out [colourtable_16_names -forcecolour $opt_forcecolour] \n + } + main { + append out "216 colours\n" + append out [colourtable_216_names -forcecolour $opt_forcecolour] \n + } + note { + append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" \n + append out " grey vs gray (UK/US spelling) - these are inconsistent for historical reasons. e.g grey0,lightslategrey,darkslategray1" \n + } + greyscale { + append out "24 greyscale colours\n" + append out [colourtable_24_names -forcecolour $opt_forcecolour] \n + } pastel { - append out \n append out "Pastel Colour Space (punk::ansi::colourtable_term_pastel)\n" - append out [colourtable_term_pastel -forcecolour $opt_forcecolour] + append out [colourtable_term_pastel -forcecolour $opt_forcecolour] \n } rainbow { - append out \n append out "Rainbow Colours (punk::ansi::colourtable_term_rainbow)\n" - append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] + append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] \n + } + default { + #only reachable if punk::args definition is out of sync + set panelnames {16 main greyscale pastel rainbow note} + append out "(ERROR: unrecognised panel '$ta' for 'a? term'. Known values $panelnames)" } } } - append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" return $out } web { - return [colourtable_web -forcecolour $opt_forcecolour -groups [lrange $args 1 end]] + set argd [punk::args::parse $args -form "web" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received panel]} { + set panels [dict get $values panel] + } else { + set panels {*} + } + return [colourtable_web -forcecolour $opt_forcecolour -groups $panels] + } + tk - TK { + set argd [punk::args::parse $args -form "tk" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received glob]} { + set globs [dict get $values glob] + } else { + set globs {*} + } + if {[dict exists $received -merged]} { + set ismerged 1 + } else { + set ismerged 0 + } + return [colourtable_tk -merged $ismerged -forcecolour $opt_forcecolour -globs $globs] } x11 { + set argd [punk::args::parse $args -form "x11" -errorstyle standard withid ::punk::ansi::a?] set out "" append out " Mostly same as web - known differences displayed" \n append out [colourtable_x11diff -forcecolour $opt_forcecolour] @@ -2243,10 +2519,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set resultlist [list] foreach i $args { - set f4 [tcl::string::range $i 0 3] + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [::split $i "-# "] 0] set s [a+ {*}$fc $i]sample - switch -- $f4 { - web- - Web- - WEB- { + switch -- $pfx { + web - Web - WEB { set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] set cont [string range $tail end-11 end] switch -- $cont { @@ -2275,7 +2552,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i $descr $s [ansistring VIEW $s]] } term - Term - undt { - set tail [tcl::string::trim [tcl::string::range $i 4 end] -] + set tail [tcl::string::range $i 5 end] if {[tcl::string::is integer -strict $tail]} { if {$tail < 256} { set descr "[tcl::dict::get $TERM_colour_map_reverse $tail]" @@ -2292,10 +2569,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - x11- - X11- { - set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] - if {[tcl::dict::exists $X11_colour_map $tail]} { - set dec [tcl::dict::get $X11_colour_map $tail] + x11 - X11 { + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] + set cont [string range $cname end-11 end] + switch -exact -- $cont {-contrasting - -contrastive {set cname [string range $tail end-12]}} + + if {[tcl::dict::exists $X11_colour_map $cname]} { + set dec [tcl::dict::get $X11_colour_map $cname] set hex [colour_dec2hex $dec] set descr "$hex $dec" } else { @@ -2303,12 +2583,27 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - rgb- - Rgb- - RGB- - - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 - - rgb# - Rgb# - RGB# - - und# - und- { + tk - Tk { + set tail [tcl::string::tolower [tcl::string::range $i 3 end]] + set cont [string range $tail end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set cname [string range $tail 0 end-12] + } + default { + set cname $tail + } + } + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set dec [tcl::dict::get $TK_colour_map_lookup $cname] + set hex [colour_dec2hex $dec] + set descr "$hex $dec" + } else { + set descr "UNKNOWN colour for tk" + } + $t add_row [list $i $descr $s [ansistring VIEW $s]] + } + rgb - Rgb - RGB - und { set cont [string range $i end-11 end] switch -- $cont { -contrasting - -contrastive { @@ -2339,7 +2634,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } set info "$hexfinal $decfinal" ;#show opposite type as first line of info col } else { - set tail [tcl::string::trim [tcl::string::range $iplain 3 end] -] + set tail [tcl::string::range $iplain 4 end] set dec $tail switch -- $cont { -contrasting { @@ -2369,15 +2664,23 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend x11colours $c } } + if {[dict exists $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal]} { + set tkcolours [dict get $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal] + } else { + set tkcolours [list] + } foreach c $webcolours { append info \n web-$c } foreach c $x11colours { append info \n x11-$c } + foreach c $tkcolours { + append info \n tk-$c + } $t add_row [list $i "$info" $s [ansistring VIEW $s]] } - unde { + default { switch -- $i { undercurly - undercurl - underdotted - underdot - underdashed - underdash - undersingle - underdouble { $t add_row [list $i extended $s [ansistring VIEW $s]] @@ -2389,19 +2692,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i "SGR 59" $s [ansistring VIEW $s]] } default { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] - } - } - } - default { - if {[tcl::string::is integer -strict $i]} { - set rmap [lreverse $SGR_map] - $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] - } else { - if {[tcl::dict::exists $SGR_map $i]} { - $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] - } else { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + #$t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + if {[tcl::string::is integer -strict $i]} { + set rmap [lreverse $SGR_map] + $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] + } else { + if {[tcl::dict::exists $SGR_map $i]} { + $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] + } else { + $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + } + } } } } @@ -2541,24 +2842,19 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes needing to go in own escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + set pfx [lindex [::split $i "-# "] 0] + #set f4 [tcl::string::range $i 0 3] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour - set tail [tcl::string::tolower [tcl::string::range $i 4 end]] + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] #-contrasting #-contrastive - set cont [string range $tail end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set cname [string range $tail 0 end-12] - } - default { - set cname $tail - } - } + set cont [string range $cname end-11 end] + switch -- $cont { -contrasting - -contrastive {set cname [string range $cname 0 end-12]} } + if {[tcl::dict::exists $WEB_colour_map $cname]} { set rgbdash [tcl::dict::get $WEB_colour_map $cname] switch -- $cont { @@ -2577,7 +2873,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a+ $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -2609,140 +2905,94 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a+ $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6 } + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - #TODO - fix - # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. - # need to emit in - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #NOTE struct::set result order can differ depending on whether tcl/critcl imp used - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline {lappend t 4} + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #NOTE struct::set result order can differ depending on whether tcl/critcl imp used + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + #TODO - fix + # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. + # need to emit in + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund {lappend t 59} + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer @@ -2772,105 +3022,112 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb foreground/background - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set iplain [string range $i 0 end-12] + rgb - Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground/background + #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set iplain [string range $i 0 end-12] + } + default { + set iplain $i + } } - default { - set iplain $i + set rgbspec [tcl::string::range $iplain 4 end] + set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - } - set rgbspec [tcl::string::trim [tcl::string::range $iplain 3 end] -] - set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + if {[tcl::string::index $i 0] eq "r"} { + #fg + lappend t "38;2;$rgbfinal" + } else { + #bg + lappend t "48;2;$rgbfinal" } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + + } elseif {$utype eq "#"} { + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - default { - set rgbfinal [join $RGB {;}] + if {[tcl::string::index $i 0] eq "r"} { + #hex rgb foreground + lappend t "38;2;$rgbfinal" + } else { + #hex rgb background + lappend t "48;2;$rgbfinal" } - } - if {[tcl::string::index $i 0] eq "r"} { - #fg - lappend t "38;2;$rgbfinal" } else { - #bg - lappend t "48;2;$rgbfinal" + puts stderr "punk::ansi::a+ ansi term rgb colour unmatched: '$i' in call 'a+ $args'" } } - "rgb#" - "Rgb#" - "RGB#" { - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx + #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb + set rgbspec [tcl::string::range $i 4 end] + set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] + #puts "---->'$RGB'<----" + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - default { - set rgbfinal [join $RGB {;}] + #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? + lappend e "58:2::$rgbfinal" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - } - if {[tcl::string::index $i 0] eq "r"} { - #hex rgb foreground - lappend t "38;2;$rgbfinal" + lappend e "58:2::$rgbfinal" } else { - #hex rgb background - lappend t "48;2;$rgbfinal" - } - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] - #puts "---->'$RGB'<----" - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } - } - #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? - lappend e "58:2::$rgbfinal" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } + puts stderr "punk::ansi::a+ ansi term underline colour unmatched: '$i' in call 'a+ $args'" } - lappend e "58:2::$rgbfinal" } undt { #CSI 58:5 UNDERLINE COLOR PALETTE INDEX @@ -2878,7 +3135,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -2889,7 +3146,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2898,10 +3155,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i' in call 'a+ $args'" + puts stderr "ansi x11 foreground colour unmatched: '$i' in call 'a+ $args'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2910,7 +3167,59 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + #foreground tk names + variable TK_colour_map_lookup ;#use the dict with added lowercase versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -exact -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i' in call 'a+ $args'" + } + } + Tk - TK { + #background X11 names + variable TK_colour_map_lookup ;#with lc versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -2919,7 +3228,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a+ ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- tk- term- rgb# rgb-" } } } @@ -2974,6 +3283,32 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #indent of 1 space is important for clarity in i -return string a+ output dict set SGR_samples $k " [punk::ansi::a+ $k]sample\x1b\[m" } + set SGR_help\ + {SGR code from the list below, or an integer corresponding to the code e.g 31 = red. + A leading capital letter indicates a codename applies to the background colour. + Other accepted codes are: + term- Term- foreground/background where int is 0-255 terminal color + term- Term- foreground/background + + web- Web- + + x11- X11- + + tk- Tk- + + rgb--- Rgb--- foreground/background where are the + 0-255 int values for red, green and blue. + rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 + + The acceptable values for colours can be queried using + punk::ansi::a? term + punk::ansi::a? web + punk::ansi::a? x11 + punk::ansi::a? tk + + Example to set foreground red and background cyan followed by a reset: + set str \"[a+ red Cyan]sample text[a]\" + } lappend PUNKARGS [list { @id -id ::punk::ansi::a+ @cmd -name "punk::ansi::a+" -help\ @@ -2981,28 +3316,25 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Unlike punk::ansi::a - it is not prefixed with an ANSI reset. " @values -min 0 -max -1 - } [string map [list [dict keys $SGR_map] $SGR_samples] { - code -type string -optional 1 -multiple 1 -choices {}\ - -choicelabels {}\ + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ - "SGR code from the list below, or an integer corresponding to the code e.g 31 = red. - A leading capital letter indicates a codename applies to the background colour. - Other accepted codes are: - term- Term- foreground/background where int is 0-255 terminal color - term- Term- foreground/background - rgb--- Rgb--- foreground/background where are the - 0-255 int values for red, green and blue. - rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 - web- Web- - - The acceptable values for and can be queried using - punk::ansi::a? term - and - punk::ansi::a? web - - Example to set foreground red and background cyan followed by a reset: - set str \"[a+ red Cyan]sample text[a]\" + "%SGR_help%" + }]] + + lappend PUNKARGS [list { + @id -id ::punk::ansi::a + @cmd -name "punk::ansi::a" -help\ + "Returns an ANSI sgr escape sequence based on the list of supplied codes. + Unlike punk::ansi::a+ - it is prefixed with an ANSI reset. " + @values -min 0 -max -1 + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ + -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ + "%SGR_help%" }]] proc a {args} { @@ -3027,6 +3359,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #we want this to be available to call even if ansi is off variable WEB_colour_map variable TERM_colour_map + variable TK_colour_map_lookup ;#Tk accepts lowercase versions of colours even though some colours are documented with casing set colour_disabled 0 #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache -action clear @@ -3044,9 +3377,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes will suppress standard SGR colours and attributes if merged in same escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [split $i "-# "] 0] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour @@ -3059,7 +3393,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -3070,142 +3404,100 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6} + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline { + lappend t 4 ;#underline + } + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund { + lappend t 59 + } + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend t "38;5;$cc" } else { @@ -3219,7 +3511,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Term - TERM { #variable TERM_colour_map #256 colour background by Xterm name or by integer - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] && $cc < 256} { lappend t "48;5;$cc" } else { @@ -3230,49 +3522,63 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 { - #decimal rgb foreground - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "38;2;$rgb" - } - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb background - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "48;2;$rgb" - } - "rgb#" { - #hex rgb foreground - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "38;2;$rgb" - } - "Rgb#" - "RGB#" { - #hex rgb background - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "48;2;$rgb" - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] - lappend e "58:2::$rgb" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - lappend e "58:2::$rgb" + rgb { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground + #form: rgb-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "38;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb foreground + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi rgb foreground colour unmatched: '$i' in call 'a $args'" + } + } + Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb background + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "48;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb background + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Rgb background colour unmatched: '$i' in call 'a $args'" + } + } + und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 {} + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #form: und-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] + lappend e "58:2::$rgb" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + lappend e "58:2::$rgb" + } else { + puts stderr "ansi underline colour unmatched: '$i' in call 'a $args'" + } } undt { #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + #undt-<0-255> or undt- + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -3283,7 +3589,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3292,10 +3598,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i'" + puts stderr "ansi x11 foreground colour unmatched: '$i'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3304,7 +3610,31 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + variable TK_colour_map_lookup + #foreground tk names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i'" + } + } + Tk - TK { + variable TK_colour_map_lookup + #background X11 names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -3313,7 +3643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" } } } @@ -3356,7 +3686,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend PUNKARGS [list { @id -id ::punk::ansi::ansiwrap - @cmd -name punk::ansi::ansiwrap -help\ + @cmd -name punk::ansi::ansiwrap\ + -summary\ + "Wrap a string with ANSI codes applied when not overridden by ANSI in the source string."\ + -help\ {Wrap a string with ANSI codes from supplied codelist(s) followed by trailing ANSI reset. The wrapping is done such that @@ -3395,12 +3728,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu -rawansi -type ansi -default "" -resetcodes -type list -default {reset} -rawresets -type ansi -default "" - -fullcodemerge -type boolean -default 0 -help\ - "experimental" -overridecodes -type list -default {} -rawoverrides -type ansi -default "" @values -min 1 -max 1 - text -type string -help\ + text -type any -help\ "String to wrap with ANSI (SGR)" }] proc ansiwrap {args} { @@ -3411,13 +3742,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } #we know there are no valid codes that start with - if {[lsearch [lrange $args 0 end-1] -*] == -1} { - #no opts - set text [lindex $args end] - set codelists [lrange $args 0 end-1] - set R [a] ;#plain ansi reset + #no opts - skip args parser + #maint: keep defaults in sync with definition above + set codelists $args + set text [lpop codelists] + set R [a] ;#plain ansi reset (equiv of default "reset") set rawansi "" set rawresets "" - set fullmerge 0 set overrides "" set rawoverrides "" } else { @@ -3428,7 +3759,6 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rawansi [dict get $opts -rawansi] set R [a+ {*}[dict get $opts -resetcodes]] set rawresets [dict get $opts -rawresets] - set fullmerge [dict get $opts -fullcodemerge] set overrides [punk::ansi::ta::get_codes_single [a+ {*}[dict get $opts -overridecodes]]] set rawoverrides [punk::ansi::ta::get_codes_single [dict get $opts -rawoverrides]] } @@ -3437,22 +3767,18 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes set codes [concat {*}$codelists] ;#flatten set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] if {$rawansi ne ""} { set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] - if {$fullmerge} { - set base [punk::ansi::codetype::sgr_merge [list $base {*}$rawcodes]] - } else { - set base [punk::ansi::codetype::sgr_merge_singles [list $base {*}$rawcodes]] - } + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] } if {$rawresets ne ""} { set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] - if {$fullmerge} { - set R [punk::ansi::codetype::sgr_merge [list $R {*}$rawresetcodes]] - } else { - set R [punk::ansi::codetype::sgr_merge_singles [list $R {*}$rawresetcodes]] - } + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] } + if {$rawoverrides ne ""} { set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] set overrides [list {*}$overrides {*}$rawoverridecodes] @@ -3474,20 +3800,105 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set codestack [list] } else { #append emit [lindex $o_codestack 0]$pt - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + #parts ends on a pt - last code always empty string + if {$code ne ""} { + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $codestack $code] + set codestack [lremove $codestack {*}$dup_posns] + lappend codestack $code } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } } } + default { + #other ansi codes + } } - default { - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit $code + } + } + return [append emit $R] + } else { + return $base$text$R + } + } + proc ansiwrap_raw {rawansi rawresets rawoverrides text} { + set codelists "" + set R "" + set overrides "" + #note that the result of any sgr_merge or sgr_merge_singles is not necessarily a single Ansi escape sequence. + #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes + set codes [concat {*}$codelists] ;#flatten + set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] + if {$rawansi ne ""} { + set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] + } + if {$rawresets ne ""} { + set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] + } + + if {$rawoverrides ne ""} { + set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] + set overrides [list {*}$overrides {*}$rawoverridecodes] + } + + set codestack [list] + if {[punk::ansi::ta::detect $text]} { + set emit "" + #set parts [punk::ansi::ta::split_codes $text] + set parts [punk::ansi::ta::split_codes_single $text] + foreach {pt code} $parts { + switch -- [llength $codestack] { + 0 { + append emit $base $pt $R + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $codestack 0]]} { + append emit $base $pt $R + set codestack [list] } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R } } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } } #parts ends on a pt - last code always empty string if {$code ne ""} { @@ -3533,6 +3944,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { return $base$text$R } + } proc ansiwrap_naive {codes text} { return [a_ {*}$codes]$text[a] @@ -4481,6 +4893,20 @@ to 223 (=255 - 32) } #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks + lappend PUNKARGS [list { + @id -id ::punk::ansi::ansistrip + @cmd -name punk::ansi::ansistrip\ + -summary\ + "Strip ANSI codes and convert VT100 graphics to unicode equivalents."\ + -help\ + "Returns a string with ANSI codes such as SGR, movements etc stripped out. + Alternate graphics chars (VT100 graphics) are replaced with modern unicode + equivalents (e.g boxdrawing glyphs). + PM, APC, SOS contents are stripped - whether or not such wrapped strings + are displayed on various terminals." + @values -min 1 -max 1 + text -type string + }] proc ansistrip {text} { #*** !doctools #[call [fun ansistrip] [arg text] ] @@ -7586,7 +8012,7 @@ tcl::namespace::eval punk::ansi::ansistring { #return pair of column extents occupied by the character index supplied. #single-width grapheme will return pair of integers of equal value - #doulbe-width grapheme will return a pair of consecutive indices + #double-width grapheme will return a pair of consecutive indices proc INDEXCOLUMNS {string idx} { #There is an index per grapheme - whether it is 1 or 2 columns wide set index [lindex [INDEXABSOLUTE $string $idx] 0] @@ -7755,6 +8181,31 @@ namespace eval punk::ansi::colour { } punk::assertion::active on + + #see also the tk function + #winfo rgb . |#XXXXXX|#XXX + #(example in punk::ansi::colourmap::get_rgb_using_tk) + + #proc percent2rgb {n} { + # # map 0..100 to a red-yellow-green sequence + # set n [expr {$n < 0? 0: $n > 100? 100: $n}] + # set red [expr {$n > 75? 60 - ($n * 15 / 25) : 15}] + # set green [expr {$n < 50? $n * 15 / 50 : 15}] + # format "#%01x%01x0" $red $green + #} ;#courtesy of RS (from tcl wiki) + proc percent2#rgb {n} { + # map 0..100 to a red-yellow-green sequence + set n [expr {$n < 0? 0: $n > 100? 100: $n}] + set red [expr {$n > 75? 1020 - ($n * 255 / 25) : 255}] + set green [expr {$n < 50? $n * 255 / 50 : 255}] + format "#%02x%02x00" $red $green + } + + proc random#rgb {} { + format #%06x [expr {int(rand() * 0xFFFFFF)}] + } + + #see also colors package #https://sourceforge.net/p/irrational-numbers/code/HEAD/tree/pkgs/Colors/trunk/colors.tcl#l159 diff --git a/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm b/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm new file mode 100644 index 00000000..6e8e28e4 --- /dev/null +++ b/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm @@ -0,0 +1,966 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2025 +# +# @@ Meta Begin +# Application ::punk::ansi::colourmap 0.1.0 +# Meta platform tcl +# Meta license MIT +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_::punk::ansi::colourmap 0 0.1.0] +#[copyright "2025"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require ::punk::ansi::colourmap] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of ::punk::ansi::colourmap +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by ::punk::ansi::colourmap +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + + +tcl::namespace::eval ::punk::ansi::colourmap { + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + # Base namespace + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap}] + #[para] Core API functions for ::punk::ansi::colourmap + #[list_begin definitions] + + variable PUNKARGS + + #---------------------------------------------- + #todo - document vars as part of package API + #- or provide a function to return varnames? + #- or wrap each in a function and see if any performance/memory impact? (readonly - so should just be a reference without any copying?) + #TK_colour_map + #TK_colour_map_lookup + #TK_colour_map_merge + #TK_colour_map_reverse + #---------------------------------------------- + + + + #significantly slower than tables - but here as a check/test + lappend PUNKARGS [list { + @id -id ::punk::ansi::colourmap::get_rgb_using_tk + @cmd -name punk::ansi::colourmap::get_rgb_using_tk -help\ + "This function requires Tk to function, and will call + 'package require tk' to load it. + The name argument accepts Tk colour names or hex values + in either #XXX or #XXXXXX format. + Tk colour names can be displayed using the command: + punk::ansi::a? tk ?glob..? + + get_rgb_using_tk returns a decimal rgb string delimited with dashes. + e.g + get_rgb_using_tk #FFF + 255-255-255 + get_rgb_using_tk SlateBlue + 106-90-205" + @leaders + name -type string|stringstartswith(#) + }] + proc get_rgb_using_tk {name} { + package require tk + #assuming 'winfo depth .' is always 32 ? + set RGB [winfo rgb . $name] + set rgb [lmap n $RGB {expr {$n / 256}}] + return [join $rgb -] + } + + variable TK_colour_map + tcl::dict::set TK_colour_map "alice blue" 240-248-255 + tcl::dict::set TK_colour_map AliceBlue 240-248-255 + tcl::dict::set TK_colour_map "antique white" 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite1 255-239-219 + tcl::dict::set TK_colour_map AntiqueWhite2 238-223-204 + tcl::dict::set TK_colour_map AntiqueWhite3 205-192-176 + tcl::dict::set TK_colour_map AntiqueWhite4 139-131-120 + tcl::dict::set TK_colour_map aqua 0-255-255 + tcl::dict::set TK_colour_map aquamarine 127-255-212 + tcl::dict::set TK_colour_map aquamarine1 127-255-212 + tcl::dict::set TK_colour_map aquamarine2 118-238-198 + tcl::dict::set TK_colour_map aquamarine3 102-205-170 + tcl::dict::set TK_colour_map aquamarine4 69-139-16 + tcl::dict::set TK_colour_map azure 240-255-255 + tcl::dict::set TK_colour_map azure1 240-255-255 + tcl::dict::set TK_colour_map azure2 224-238-238 + tcl::dict::set TK_colour_map azure3 193-205-205 + tcl::dict::set TK_colour_map azure4 131-139-139 + tcl::dict::set TK_colour_map beige 245-245-220 + tcl::dict::set TK_colour_map bisque 255-228-196 + tcl::dict::set TK_colour_map bisque1 255-228-196 + tcl::dict::set TK_colour_map bisque2 238-213-183 + tcl::dict::set TK_colour_map bisque3 205-183-158 + tcl::dict::set TK_colour_map bisque4 139-125-107 + tcl::dict::set TK_colour_map black 0-0-0 + tcl::dict::set TK_colour_map "blanched almond" 255-235-205 + tcl::dict::set TK_colour_map BlanchedAlmond 255-235-205 + tcl::dict::set TK_colour_map blue 0-0-255 + tcl::dict::set TK_colour_map "blue violet" 138-43-226 + tcl::dict::set TK_colour_map blue1 0-0-255 + tcl::dict::set TK_colour_map blue2 0-0-238 + tcl::dict::set TK_colour_map blue3 0-0-205 + tcl::dict::set TK_colour_map blue4 0-0-139 + tcl::dict::set TK_colour_map BlueViolet 138-43-226 + tcl::dict::set TK_colour_map brown 165-42-42 + tcl::dict::set TK_colour_map brown1 255-64-64 + tcl::dict::set TK_colour_map brown2 238-59-59 + tcl::dict::set TK_colour_map brown3 205-51-51 + tcl::dict::set TK_colour_map brown4 139-35-35 + tcl::dict::set TK_colour_map burlywood 222-184-135 + tcl::dict::set TK_colour_map burlywood1 255-211-155 + tcl::dict::set TK_colour_map burlywood2 238-197-145 + tcl::dict::set TK_colour_map burlywood3 205-170-125 + tcl::dict::set TK_colour_map burlywood4 139-115-85 + tcl::dict::set TK_colour_map "cadet blue" 95-158-160 + tcl::dict::set TK_colour_map CadetBlue 95-158-160 + tcl::dict::set TK_colour_map CadetBlue1 152-245-255 + tcl::dict::set TK_colour_map CadetBlue2 142-229-238 + tcl::dict::set TK_colour_map CadetBlue3 122-197-205 + tcl::dict::set TK_colour_map CadetBlue4 83-134-139 + tcl::dict::set TK_colour_map chartreuse 127-255-0 + tcl::dict::set TK_colour_map chartreuse1 127-255-0 + tcl::dict::set TK_colour_map chartreuse2 118-238-0 + tcl::dict::set TK_colour_map chartreuse3 102-205-0 + tcl::dict::set TK_colour_map chartreuse4 69-139-0 + tcl::dict::set TK_colour_map chocolate 210-105-30 + tcl::dict::set TK_colour_map chocolate1 255-127-36 + tcl::dict::set TK_colour_map chocolate2 238-118-33 + tcl::dict::set TK_colour_map chocolate3 205-102-29 + tcl::dict::set TK_colour_map chocolate4 139-69-19 + tcl::dict::set TK_colour_map coral 255-127-80 + tcl::dict::set TK_colour_map coral1 255-114-86 + tcl::dict::set TK_colour_map coral2 238-106-80 + tcl::dict::set TK_colour_map coral3 205-91-69 + tcl::dict::set TK_colour_map coral4 139-62-47 + tcl::dict::set TK_colour_map "cornflower blue" 100-149-237 + tcl::dict::set TK_colour_map CornflowerBlue 100-149-237 + tcl::dict::set TK_colour_map cornsilk 255-248-220 + tcl::dict::set TK_colour_map cornsilk1 255-248-220 + tcl::dict::set TK_colour_map cornsilk2 238-232-205 + tcl::dict::set TK_colour_map cornsilk3 205-200-177 + tcl::dict::set TK_colour_map cornsilk4 139-136-120 + tcl::dict::set TK_colour_map crimson 220-20-60 + tcl::dict::set TK_colour_map cyan 0-255-255 + tcl::dict::set TK_colour_map cyan1 0-255-255 + tcl::dict::set TK_colour_map cyan2 0-238-238 + tcl::dict::set TK_colour_map cyan3 0-205-205 + tcl::dict::set TK_colour_map cyan4 0-139-139 + tcl::dict::set TK_colour_map "dark blue" 0-0-139 + tcl::dict::set TK_colour_map "dark cyan" 0-139-139 + tcl::dict::set TK_colour_map "dark goldenrod" 184-134-11 + tcl::dict::set TK_colour_map "dark gray" 169-169-169 + tcl::dict::set TK_colour_map "dark green" 0-100-0 + tcl::dict::set TK_colour_map "dark grey" 169-169-169 + tcl::dict::set TK_colour_map "dark khaki" 189-183-107 + tcl::dict::set TK_colour_map "dark magenta" 139-0-139 + tcl::dict::set TK_colour_map "dark olive green" 85-107-47 + tcl::dict::set TK_colour_map "dark orange" 255-140-0 + tcl::dict::set TK_colour_map "dark orchid" 153-50-204 + tcl::dict::set TK_colour_map "dark red" 139-0-0 + tcl::dict::set TK_colour_map "dark salmon" 233-150-122 + tcl::dict::set TK_colour_map "dark sea green" 143-188-143 + tcl::dict::set TK_colour_map "dark slate blue" 72-61-139 + tcl::dict::set TK_colour_map "dark slate gray" 47-79-79 + tcl::dict::set TK_colour_map "dark slate grey" 47-79-79 + tcl::dict::set TK_colour_map "dark turquoise" 0-206-209 + tcl::dict::set TK_colour_map "dark violet" 148-0-211 + tcl::dict::set TK_colour_map DarkBlue 0-0-139 + tcl::dict::set TK_colour_map DarkCyan 0-139-139 + tcl::dict::set TK_colour_map DarkGoldenrod 184-134-11 + tcl::dict::set TK_colour_map DarkGoldenrod1 255-185-15 + tcl::dict::set TK_colour_map DarkGoldenrod2 238-173-14 + tcl::dict::set TK_colour_map DarkGoldenrod3 205-149-12 + tcl::dict::set TK_colour_map DarkGoldenrod4 139-101-8 + tcl::dict::set TK_colour_map DarkGray 169-169-169 + tcl::dict::set TK_colour_map DarkGreen 0-100-0 + tcl::dict::set TK_colour_map DarkGrey 169-169-169 + tcl::dict::set TK_colour_map DarkKhaki 189-183-107 + tcl::dict::set TK_colour_map DarkMagenta 139-0-139 + tcl::dict::set TK_colour_map DarkOliveGreen 85-107-47 + tcl::dict::set TK_colour_map DarkOliveGreen1 202-255-112 + tcl::dict::set TK_colour_map DarkOliveGreen2 188-238-104 + tcl::dict::set TK_colour_map DarkOliveGreen3 162-205-90 + tcl::dict::set TK_colour_map DarkOliveGreen4 110-139-61 + tcl::dict::set TK_colour_map DarkOrange 255-140-0 + tcl::dict::set TK_colour_map DarkOrange1 255-127-0 + tcl::dict::set TK_colour_map DarkOrange2 238-118-0 + tcl::dict::set TK_colour_map DarkOrange3 205-102-0 + tcl::dict::set TK_colour_map DarkOrange4 139-69-0 + tcl::dict::set TK_colour_map DarkOrchid 153-50-204 + tcl::dict::set TK_colour_map DarkOrchid1 191-62-255 + tcl::dict::set TK_colour_map DarkOrchid2 178-58-238 + tcl::dict::set TK_colour_map DarkOrchid3 154-50-205 + tcl::dict::set TK_colour_map DarkOrchid4 104-34-139 + tcl::dict::set TK_colour_map DarkRed 139-0-0 + tcl::dict::set TK_colour_map DarkSalmon 233-150-122 + tcl::dict::set TK_colour_map DarkSeaGreen 43-188-143 + tcl::dict::set TK_colour_map DarkSeaGreen1 193-255-193 + tcl::dict::set TK_colour_map DarkSeaGreen2 180-238-180 + tcl::dict::set TK_colour_map DarkSeaGreen3 155-205-155 + tcl::dict::set TK_colour_map DarkSeaGreen4 105-139-105 + tcl::dict::set TK_colour_map DarkSlateBlue 72-61-139 + tcl::dict::set TK_colour_map DarkSlateGray 47-79-79 + tcl::dict::set TK_colour_map DarkSlateGray1 151-255-255 + tcl::dict::set TK_colour_map DarkSlateGray2 141-238-238 + tcl::dict::set TK_colour_map DarkSlateGray3 121-205-205 + tcl::dict::set TK_colour_map DarkSlateGray4 82-139-139 + tcl::dict::set TK_colour_map DarkSlateGrey 47-79-79 + tcl::dict::set TK_colour_map DarkTurquoise 0-206-209 + tcl::dict::set TK_colour_map DarkViolet 148-0-211 + tcl::dict::set TK_colour_map "deep pink" 255-20-147 + tcl::dict::set TK_colour_map "deep sky blue" 0-191-255 + tcl::dict::set TK_colour_map DeepPink 255-20-147 + tcl::dict::set TK_colour_map DeepPink1 255-20-147 + tcl::dict::set TK_colour_map DeepPink2 238-18-137 + tcl::dict::set TK_colour_map DeepPink3 205-16-118 + tcl::dict::set TK_colour_map DeepPink4 139-10-80 + tcl::dict::set TK_colour_map DeepSkyBlue 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue1 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue2 0-178-238 + tcl::dict::set TK_colour_map DeepSkyBlue3 0-154-205 + tcl::dict::set TK_colour_map DeepSkyBlue4 0-104-139 + tcl::dict::set TK_colour_map "dim gray" 105-105-105 + tcl::dict::set TK_colour_map "dim grey" 105-105-105 + tcl::dict::set TK_colour_map DimGray 105-105-105 + tcl::dict::set TK_colour_map DimGrey 105-105-105 + tcl::dict::set TK_colour_map "dodger blue" 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue1 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue2 28-134-238 + tcl::dict::set TK_colour_map DodgerBlue3 24-116-205 + tcl::dict::set TK_colour_map DodgerBlue4 16-78-139 + tcl::dict::set TK_colour_map firebrick 178-34-34 + tcl::dict::set TK_colour_map firebrick1 255-48-48 + tcl::dict::set TK_colour_map firebrick2 238-44-44 + tcl::dict::set TK_colour_map firebrick3 205-38-38 + tcl::dict::set TK_colour_map firebrick4 139-26-26 + tcl::dict::set TK_colour_map "floral white" 255-250-240 + tcl::dict::set TK_colour_map FloralWhite 255-250-240 + tcl::dict::set TK_colour_map "forest green" 34-139-34 + tcl::dict::set TK_colour_map ForestGreen 34-139-34 + tcl::dict::set TK_colour_map fuchsia 255-0-255 + tcl::dict::set TK_colour_map gainsboro 220-220-220 + tcl::dict::set TK_colour_map "ghost white" 248-248-255 + tcl::dict::set TK_colour_map GhostWhite 248-248-255 + tcl::dict::set TK_colour_map gold 255-215-0 + tcl::dict::set TK_colour_map gold1 255-215-0 + tcl::dict::set TK_colour_map gold2 238-201-0 + tcl::dict::set TK_colour_map gold3 205-173-0 + tcl::dict::set TK_colour_map gold4 139-117-0 + tcl::dict::set TK_colour_map goldenrod 218-165-32 + tcl::dict::set TK_colour_map goldenrod1 255-193-37 + tcl::dict::set TK_colour_map goldenrod2 238-180-34 + tcl::dict::set TK_colour_map goldenrod3 205-155-29 + tcl::dict::set TK_colour_map goldenrod4 139-105-20 + tcl::dict::set TK_colour_map gray 128-128-128 + tcl::dict::set TK_colour_map gray0 0-0-0 + tcl::dict::set TK_colour_map gray1 3-3-3 + tcl::dict::set TK_colour_map gray2 5-5-5 + tcl::dict::set TK_colour_map gray3 8-8-8 + tcl::dict::set TK_colour_map gray4 10-10-10 + tcl::dict::set TK_colour_map gray5 13-13-13 + tcl::dict::set TK_colour_map gray6 15-15-15 + tcl::dict::set TK_colour_map gray7 18-18-18 + tcl::dict::set TK_colour_map gray8 20-20-20 + tcl::dict::set TK_colour_map gray9 23-23-23 + tcl::dict::set TK_colour_map gray10 26-26-26 + tcl::dict::set TK_colour_map gray11 28-28-28 + tcl::dict::set TK_colour_map gray12 31-31-31 + tcl::dict::set TK_colour_map gray13 33-33-33 + tcl::dict::set TK_colour_map gray14 36-36-36 + tcl::dict::set TK_colour_map gray15 38-38-38 + tcl::dict::set TK_colour_map gray16 41-41-41 + tcl::dict::set TK_colour_map gray17 43-43-43 + tcl::dict::set TK_colour_map gray18 46-46-46 + tcl::dict::set TK_colour_map gray19 48-48-48 + tcl::dict::set TK_colour_map gray20 51-51-51 + tcl::dict::set TK_colour_map gray21 54-54-54 + tcl::dict::set TK_colour_map gray22 56-56-56 + tcl::dict::set TK_colour_map gray23 59-59-59 + tcl::dict::set TK_colour_map gray24 61-61-61 + tcl::dict::set TK_colour_map gray25 64-64-64 + tcl::dict::set TK_colour_map gray26 66-66-66 + tcl::dict::set TK_colour_map gray27 69-69-69 + tcl::dict::set TK_colour_map gray28 71-71-71 + tcl::dict::set TK_colour_map gray29 74-74-74 + tcl::dict::set TK_colour_map gray30 77-77-77 + tcl::dict::set TK_colour_map gray31 79-79-79 + tcl::dict::set TK_colour_map gray32 82-82-82 + tcl::dict::set TK_colour_map gray33 84-84-84 + tcl::dict::set TK_colour_map gray34 87-87-87 + tcl::dict::set TK_colour_map gray35 89-89-89 + tcl::dict::set TK_colour_map gray36 92-92-92 + tcl::dict::set TK_colour_map gray37 94-94-94 + tcl::dict::set TK_colour_map gray38 97-97-97 + tcl::dict::set TK_colour_map gray39 99-99-99 + tcl::dict::set TK_colour_map gray40 102-102-102 + tcl::dict::set TK_colour_map gray41 105-105-105 + tcl::dict::set TK_colour_map gray42 107-107-107 + tcl::dict::set TK_colour_map gray43 110-110-110 + tcl::dict::set TK_colour_map gray44 112-112-112 + tcl::dict::set TK_colour_map gray45 115-115-115 + tcl::dict::set TK_colour_map gray46 117-117-117 + tcl::dict::set TK_colour_map gray47 120-120-120 + tcl::dict::set TK_colour_map gray48 122-122-122 + tcl::dict::set TK_colour_map gray49 125-125-125 + tcl::dict::set TK_colour_map gray50 127-127-127 + tcl::dict::set TK_colour_map gray51 130-130-130 + tcl::dict::set TK_colour_map gray52 133-133-133 + tcl::dict::set TK_colour_map gray53 135-135-135 + tcl::dict::set TK_colour_map gray54 138-138-138 + tcl::dict::set TK_colour_map gray55 140-140-140 + tcl::dict::set TK_colour_map gray56 143-143-143 + tcl::dict::set TK_colour_map gray57 145-145-145 + tcl::dict::set TK_colour_map gray58 148-148-148 + tcl::dict::set TK_colour_map gray59 150-150-150 + tcl::dict::set TK_colour_map gray60 153-153-153 + tcl::dict::set TK_colour_map gray61 156-156-156 + tcl::dict::set TK_colour_map gray62 158-158-158 + tcl::dict::set TK_colour_map gray63 161-161-161 + tcl::dict::set TK_colour_map gray64 163-163-163 + tcl::dict::set TK_colour_map gray65 166-166-166 + tcl::dict::set TK_colour_map gray66 168-168-168 + tcl::dict::set TK_colour_map gray67 171-171-171 + tcl::dict::set TK_colour_map gray68 173-173-173 + tcl::dict::set TK_colour_map gray69 176-176-176 + tcl::dict::set TK_colour_map gray70 179-179-179 + tcl::dict::set TK_colour_map gray71 181-181-181 + tcl::dict::set TK_colour_map gray72 184-184-184 + tcl::dict::set TK_colour_map gray73 186-186-186 + tcl::dict::set TK_colour_map gray74 189-189-189 + tcl::dict::set TK_colour_map gray75 191-191-191 + tcl::dict::set TK_colour_map gray76 194-194-194 + tcl::dict::set TK_colour_map gray77 196-196-196 + tcl::dict::set TK_colour_map gray78 199-199-199 + tcl::dict::set TK_colour_map gray79 201-201-201 + tcl::dict::set TK_colour_map gray80 204-204-204 + tcl::dict::set TK_colour_map gray81 207-207-207 + tcl::dict::set TK_colour_map gray82 209-209-209 + tcl::dict::set TK_colour_map gray83 212-212-212 + tcl::dict::set TK_colour_map gray84 214-214-214 + tcl::dict::set TK_colour_map gray85 217-217-217 + tcl::dict::set TK_colour_map gray86 219-219-219 + tcl::dict::set TK_colour_map gray87 222-222-222 + tcl::dict::set TK_colour_map gray88 224-224-224 + tcl::dict::set TK_colour_map gray89 227-227-227 + tcl::dict::set TK_colour_map gray90 229-229-229 + tcl::dict::set TK_colour_map gray91 232-232-232 + tcl::dict::set TK_colour_map gray92 235-235-235 + tcl::dict::set TK_colour_map gray93 237-237-237 + tcl::dict::set TK_colour_map gray94 240-240-240 + tcl::dict::set TK_colour_map gray95 242-242-242 + tcl::dict::set TK_colour_map gray96 245-245-245 + tcl::dict::set TK_colour_map gray97 247-247-247 + tcl::dict::set TK_colour_map gray98 250-250-250 + tcl::dict::set TK_colour_map gray99 252-252-252 + tcl::dict::set TK_colour_map gray100 255-255-255 + tcl::dict::set TK_colour_map green 0-128-0 + tcl::dict::set TK_colour_map "green yellow" 173-255-47 + tcl::dict::set TK_colour_map green1 0-255-0 + tcl::dict::set TK_colour_map green2 0-238-0 + tcl::dict::set TK_colour_map green3 0-205-0 + tcl::dict::set TK_colour_map green4 0-139-0 + tcl::dict::set TK_colour_map GreenYellow 173-255-47 + tcl::dict::set TK_colour_map grey 128-128-128 + tcl::dict::set TK_colour_map grey0 0-0-0 + tcl::dict::set TK_colour_map grey1 3-3-3 + tcl::dict::set TK_colour_map grey2 5-5-5 + tcl::dict::set TK_colour_map grey3 8-8-8 + tcl::dict::set TK_colour_map grey4 10-10-10 + tcl::dict::set TK_colour_map grey5 13-13-13 + tcl::dict::set TK_colour_map grey6 15-15-15 + tcl::dict::set TK_colour_map grey7 18-18-18 + tcl::dict::set TK_colour_map grey8 20-20-20 + tcl::dict::set TK_colour_map grey9 23-23-23 + tcl::dict::set TK_colour_map grey10 26-26-26 + tcl::dict::set TK_colour_map grey11 28-28-28 + tcl::dict::set TK_colour_map grey12 31-31-31 + tcl::dict::set TK_colour_map grey13 33-33-33 + tcl::dict::set TK_colour_map grey14 36-36-36 + tcl::dict::set TK_colour_map grey15 38-38-38 + tcl::dict::set TK_colour_map grey16 41-41-41 + tcl::dict::set TK_colour_map grey17 43-43-43 + tcl::dict::set TK_colour_map grey18 46-46-46 + tcl::dict::set TK_colour_map grey19 48-48-48 + tcl::dict::set TK_colour_map grey20 51-51-51 + tcl::dict::set TK_colour_map grey21 54-54-54 + tcl::dict::set TK_colour_map grey22 56-56-56 + tcl::dict::set TK_colour_map grey23 59-59-59 + tcl::dict::set TK_colour_map grey24 61-61-61 + tcl::dict::set TK_colour_map grey25 64-64-64 + tcl::dict::set TK_colour_map grey26 66-66-66 + tcl::dict::set TK_colour_map grey27 69-69-69 + tcl::dict::set TK_colour_map grey28 71-71-71 + tcl::dict::set TK_colour_map grey29 74-74-74 + tcl::dict::set TK_colour_map grey30 77-77-77 + tcl::dict::set TK_colour_map grey31 79-79-79 + tcl::dict::set TK_colour_map grey32 82-82-82 + tcl::dict::set TK_colour_map grey33 84-84-84 + tcl::dict::set TK_colour_map grey34 87-87-87 + tcl::dict::set TK_colour_map grey35 89-89-89 + tcl::dict::set TK_colour_map grey36 92-92-92 + tcl::dict::set TK_colour_map grey37 94-94-94 + tcl::dict::set TK_colour_map grey38 97-97-97 + tcl::dict::set TK_colour_map grey39 99-99-99 + tcl::dict::set TK_colour_map grey40 102-102-102 + tcl::dict::set TK_colour_map grey41 105-105-105 + tcl::dict::set TK_colour_map grey42 107-107-107 + tcl::dict::set TK_colour_map grey43 110-110-110 + tcl::dict::set TK_colour_map grey44 112-112-112 + tcl::dict::set TK_colour_map grey45 115-115-115 + tcl::dict::set TK_colour_map grey46 117-117-117 + tcl::dict::set TK_colour_map grey47 120-120-120 + tcl::dict::set TK_colour_map grey48 122-122-122 + tcl::dict::set TK_colour_map grey49 125-125-125 + tcl::dict::set TK_colour_map grey50 127-127-127 + tcl::dict::set TK_colour_map grey51 130-130-130 + tcl::dict::set TK_colour_map grey52 133-133-133 + tcl::dict::set TK_colour_map grey53 135-135-135 + tcl::dict::set TK_colour_map grey54 138-138-138 + tcl::dict::set TK_colour_map grey55 140-140-140 + tcl::dict::set TK_colour_map grey56 143-143-143 + tcl::dict::set TK_colour_map grey57 145-145-145 + tcl::dict::set TK_colour_map grey58 148-148-148 + tcl::dict::set TK_colour_map grey59 150-150-150 + tcl::dict::set TK_colour_map grey60 153-153-153 + tcl::dict::set TK_colour_map grey61 156-156-156 + tcl::dict::set TK_colour_map grey62 158-158-158 + tcl::dict::set TK_colour_map grey63 161-161-161 + tcl::dict::set TK_colour_map grey64 163-163-163 + tcl::dict::set TK_colour_map grey65 166-166-166 + tcl::dict::set TK_colour_map grey66 168-168-168 + tcl::dict::set TK_colour_map grey67 171-171-171 + tcl::dict::set TK_colour_map grey68 173-173-173 + tcl::dict::set TK_colour_map grey69 176-176-176 + tcl::dict::set TK_colour_map grey70 179-179-179 + tcl::dict::set TK_colour_map grey71 181-181-181 + tcl::dict::set TK_colour_map grey72 184-184-184 + tcl::dict::set TK_colour_map grey73 186-186-186 + tcl::dict::set TK_colour_map grey74 189-189-189 + tcl::dict::set TK_colour_map grey75 191-191-191 + tcl::dict::set TK_colour_map grey76 194-194-194 + tcl::dict::set TK_colour_map grey77 196-196-196 + tcl::dict::set TK_colour_map grey78 199-199-199 + tcl::dict::set TK_colour_map grey79 201-201-201 + tcl::dict::set TK_colour_map grey80 204-204-204 + tcl::dict::set TK_colour_map grey81 207-207-207 + tcl::dict::set TK_colour_map grey82 209-209-209 + tcl::dict::set TK_colour_map grey83 212-212-212 + tcl::dict::set TK_colour_map grey84 214-214-214 + tcl::dict::set TK_colour_map grey85 217-217-217 + tcl::dict::set TK_colour_map grey86 219-219-219 + tcl::dict::set TK_colour_map grey87 222-222-222 + tcl::dict::set TK_colour_map grey88 224-224-224 + tcl::dict::set TK_colour_map grey89 227-227-227 + tcl::dict::set TK_colour_map grey90 229-229-229 + tcl::dict::set TK_colour_map grey91 232-232-232 + tcl::dict::set TK_colour_map grey92 235-235-235 + tcl::dict::set TK_colour_map grey93 237-237-237 + tcl::dict::set TK_colour_map grey94 240-240-240 + tcl::dict::set TK_colour_map grey95 242-242-242 + tcl::dict::set TK_colour_map grey96 245-245-245 + tcl::dict::set TK_colour_map grey97 247-247-247 + tcl::dict::set TK_colour_map grey98 250-250-250 + tcl::dict::set TK_colour_map grey99 252-252-252 + tcl::dict::set TK_colour_map grey100 255-255-255 + tcl::dict::set TK_colour_map honeydew 240-255-240 + tcl::dict::set TK_colour_map honeydew1 240-255-240 + tcl::dict::set TK_colour_map honeydew2 224-238-224 + tcl::dict::set TK_colour_map honeydew3 193-205-193 + tcl::dict::set TK_colour_map honeydew4 131-139-131 + tcl::dict::set TK_colour_map "hot pink" 255-105-180 + tcl::dict::set TK_colour_map HotPink 255-105-180 + tcl::dict::set TK_colour_map HotPink1 255-110-180 + tcl::dict::set TK_colour_map HotPink2 238-106-167 + tcl::dict::set TK_colour_map HotPink3 205-96-144 + tcl::dict::set TK_colour_map HotPink4 139-58-98 + tcl::dict::set TK_colour_map "indian red" 205-92-92 + tcl::dict::set TK_colour_map IndianRed 205-92-92 + tcl::dict::set TK_colour_map IndianRed1 255-106-106 + tcl::dict::set TK_colour_map IndianRed2 238-99-99 + tcl::dict::set TK_colour_map IndianRed3 205-85-85 + tcl::dict::set TK_colour_map IndianRed4 139-58-58 + tcl::dict::set TK_colour_map indigo 75-0-130 + tcl::dict::set TK_colour_map ivory 255-255-240 + tcl::dict::set TK_colour_map ivory1 255-255-240 + tcl::dict::set TK_colour_map ivory2 238-238-224 + tcl::dict::set TK_colour_map ivory3 205-205-193 + tcl::dict::set TK_colour_map ivory4 139-139-131 + tcl::dict::set TK_colour_map khaki 240-230-140 + tcl::dict::set TK_colour_map khaki1 255-246-143 + tcl::dict::set TK_colour_map khaki2 238-230-133 + tcl::dict::set TK_colour_map khaki3 205-198-115 + tcl::dict::set TK_colour_map khaki4 139-134-78 + tcl::dict::set TK_colour_map lavender 230-230-250 + tcl::dict::set TK_colour_map "lavender blush" 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush1 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush2 238-224-229 + tcl::dict::set TK_colour_map LavenderBlush3 205-193-197 + tcl::dict::set TK_colour_map LavenderBlush4 139-131-134 + tcl::dict::set TK_colour_map "lawn green" 124-252-0 + tcl::dict::set TK_colour_map LawnGreen 124-252-0 + tcl::dict::set TK_colour_map "lemon chiffon" 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon1 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon2 238-233-191 + tcl::dict::set TK_colour_map LemonChiffon3 205-201-165 + tcl::dict::set TK_colour_map LemonChiffon4 139-137-112 + tcl::dict::set TK_colour_map "light blue" 173-216-230 + tcl::dict::set TK_colour_map "light coral" 240-128-128 + tcl::dict::set TK_colour_map "light cyan" 224-255-255 + tcl::dict::set TK_colour_map "light goldenrod" 238-221-130 + tcl::dict::set TK_colour_map "light goldenrod yellow" 250-250-210 + tcl::dict::set TK_colour_map "light gray" 211-211-211 + tcl::dict::set TK_colour_map "light green" 144-238-144 + tcl::dict::set TK_colour_map "light grey" 211-211-211 + tcl::dict::set TK_colour_map "light pink" 255-182-193 + tcl::dict::set TK_colour_map "light salmon" 255-160-122 + tcl::dict::set TK_colour_map "light sea green" 32-178-170 + tcl::dict::set TK_colour_map "light sky blue" 135-206-250 + tcl::dict::set TK_colour_map "light slate blue" 132-112-255 + tcl::dict::set TK_colour_map "light slate gray" 119-136-153 + tcl::dict::set TK_colour_map "light slate grey" 119-136-153 + tcl::dict::set TK_colour_map "light steel blue" 176-196-222 + tcl::dict::set TK_colour_map "light yellow" 255-255-224 + tcl::dict::set TK_colour_map LightBlue 173-216-230 + tcl::dict::set TK_colour_map LightBlue1 191-239-255 + tcl::dict::set TK_colour_map LightBlue2 178-223-238 + tcl::dict::set TK_colour_map LightBlue3 154-192-205 + tcl::dict::set TK_colour_map LightBlue4 104-131-139 + tcl::dict::set TK_colour_map LightCoral 240-128-128 + tcl::dict::set TK_colour_map LightCyan 224-255-255 + tcl::dict::set TK_colour_map LightCyan1 224-255-255 + tcl::dict::set TK_colour_map LightCyan2 209-238-238 + tcl::dict::set TK_colour_map LightCyan3 180-205-205 + tcl::dict::set TK_colour_map LightCyan4 122-139-139 + tcl::dict::set TK_colour_map LightGoldenrod 238-221-130 + tcl::dict::set TK_colour_map LightGoldenrod1 255-236-139 + tcl::dict::set TK_colour_map LightGoldenrod2 238-220-130 + tcl::dict::set TK_colour_map LightGoldenrod3 205-190-112 + tcl::dict::set TK_colour_map LightGoldenrod4 139-129-76 + tcl::dict::set TK_colour_map LightGoldenrodYellow 250-250-210 + tcl::dict::set TK_colour_map LightGray 211-211-211 + tcl::dict::set TK_colour_map LightGreen 144-238-144 + tcl::dict::set TK_colour_map LightGrey 211-211-211 + tcl::dict::set TK_colour_map LightPink 255-182-193 + tcl::dict::set TK_colour_map LightPink1 255-174-185 + tcl::dict::set TK_colour_map LightPink2 238-162-173 + tcl::dict::set TK_colour_map LightPink3 205-140-149 + tcl::dict::set TK_colour_map LightPink4 139-95-101 + tcl::dict::set TK_colour_map LightSalmon 255-160-122 + tcl::dict::set TK_colour_map LightSalmon1 255-160-122 + tcl::dict::set TK_colour_map LightSalmon2 238-149-114 + tcl::dict::set TK_colour_map LightSalmon3 205-129-98 + tcl::dict::set TK_colour_map LightSalmon4 139-87-66 + tcl::dict::set TK_colour_map LightSeaGreen 32-178-170 + tcl::dict::set TK_colour_map LightSkyBlue 135-206-250 + tcl::dict::set TK_colour_map LightSkyBlue1 176-226-255 + tcl::dict::set TK_colour_map LightSkyBlue2 164-211-238 + tcl::dict::set TK_colour_map LightSkyBlue3 141-182-205 + tcl::dict::set TK_colour_map LightSkyBlue4 96-123-139 + tcl::dict::set TK_colour_map LightSlateBlue 132-112-255 + tcl::dict::set TK_colour_map LightSlateGray 119-136-153 + tcl::dict::set TK_colour_map LightSlateGrey 119-136-153 + tcl::dict::set TK_colour_map LightSteelBlue 176-196-222 + tcl::dict::set TK_colour_map LightSteelBlue1 202-225-255 + tcl::dict::set TK_colour_map LightSteelBlue2 188-210-238 + tcl::dict::set TK_colour_map LightSteelBlue3 162-181-205 + tcl::dict::set TK_colour_map LightSteelBlue4 110-123-139 + tcl::dict::set TK_colour_map LightYellow 255-255-224 + tcl::dict::set TK_colour_map LightYellow1 255-255-224 + tcl::dict::set TK_colour_map LightYellow2 238-238-209 + tcl::dict::set TK_colour_map LightYellow3 205-205-180 + tcl::dict::set TK_colour_map LightYellow4 139-139-122 + tcl::dict::set TK_colour_map lime 0-255-0 + tcl::dict::set TK_colour_map "lime green" 50-205-50 + tcl::dict::set TK_colour_map LimeGreen 50-205-50 + tcl::dict::set TK_colour_map linen 250-240-230 + tcl::dict::set TK_colour_map magenta 255-0-255 + tcl::dict::set TK_colour_map magenta1 255-0-255 + tcl::dict::set TK_colour_map magenta2 238-0-238 + tcl::dict::set TK_colour_map magenta3 205-0-205 + tcl::dict::set TK_colour_map magenta4 139-0-139 + tcl::dict::set TK_colour_map maroon 128-0-0 + tcl::dict::set TK_colour_map maroon1 255-52-179 + tcl::dict::set TK_colour_map maroon2 238-48-167 + tcl::dict::set TK_colour_map maroon3 205-41-144 + tcl::dict::set TK_colour_map maroon4 139-28-98 + tcl::dict::set TK_colour_map "medium aquamarine" 102-205-170 + tcl::dict::set TK_colour_map "medium blue" 0-0-205 + tcl::dict::set TK_colour_map "medium orchid" 186-85-211 + tcl::dict::set TK_colour_map "medium purple" 147-112-219 + tcl::dict::set TK_colour_map "medium sea green" 60-179-113 + tcl::dict::set TK_colour_map "medium slate blue" 123-104-238 + tcl::dict::set TK_colour_map "medium spring green" 0-250-154 + tcl::dict::set TK_colour_map "medium turquoise" 72-209-204 + tcl::dict::set TK_colour_map "medium violet red" 199-21-133 + tcl::dict::set TK_colour_map MediumAquamarine 102-205-170 + tcl::dict::set TK_colour_map MediumBlue 0-0-205 + tcl::dict::set TK_colour_map MediumOrchid 186-85-211 + tcl::dict::set TK_colour_map MediumOrchid1 224-102-255 + tcl::dict::set TK_colour_map MediumOrchid2 209-95-238 + tcl::dict::set TK_colour_map MediumOrchid3 180-82-205 + tcl::dict::set TK_colour_map MediumOrchid4 122-55-139 + tcl::dict::set TK_colour_map MediumPurple 147-112-219 + tcl::dict::set TK_colour_map MediumPurple1 171-130-255 + tcl::dict::set TK_colour_map MediumPurple2 159-121-238 + tcl::dict::set TK_colour_map MediumPurple3 137-104-205 + tcl::dict::set TK_colour_map MediumPurple4 93-71-139 + tcl::dict::set TK_colour_map MediumSeaGreen 60-179-113 + tcl::dict::set TK_colour_map MediumSlateBlue 123-104-238 + tcl::dict::set TK_colour_map MediumSpringGreen 0-250-154 + tcl::dict::set TK_colour_map MediumTurquoise 72-209-204 + tcl::dict::set TK_colour_map MediumVioletRed 199-21-133 + tcl::dict::set TK_colour_map "midnight blue" 25-25-112 + tcl::dict::set TK_colour_map MidnightBlue 25-25-112 + tcl::dict::set TK_colour_map "mint cream" 245-255-250 + tcl::dict::set TK_colour_map MintCream 245-255-250 + tcl::dict::set TK_colour_map "misty rose" 255-228-225 + tcl::dict::set TK_colour_map MistyRose 255-228-225 + tcl::dict::set TK_colour_map MistyRose1 255-228-225 + tcl::dict::set TK_colour_map MistyRose2 238-213-210 + tcl::dict::set TK_colour_map MistyRose3 205-183-181 + tcl::dict::set TK_colour_map MistyRose4 139-125-123 + tcl::dict::set TK_colour_map moccasin 255-228-181 + tcl::dict::set TK_colour_map "navajo white" 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite1 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite2 238-207-161 + tcl::dict::set TK_colour_map NavajoWhite3 205-179-139 + tcl::dict::set TK_colour_map NavajoWhite4 139-121-94 + tcl::dict::set TK_colour_map navy 0-0-128 + tcl::dict::set TK_colour_map "navy blue" 0-0-128 + tcl::dict::set TK_colour_map NavyBlue 0-0-128 + tcl::dict::set TK_colour_map "old lace" 253-245-230 + tcl::dict::set TK_colour_map OldLace 253-245-230 + tcl::dict::set TK_colour_map olive 128-128-0 + tcl::dict::set TK_colour_map "olive drab" 107-142-35 + tcl::dict::set TK_colour_map OliveDrab 107-142-35 + tcl::dict::set TK_colour_map OliveDrab1 192-255-62 + tcl::dict::set TK_colour_map OliveDrab2 179-238-58 + tcl::dict::set TK_colour_map OliveDrab3 154-205-50 + tcl::dict::set TK_colour_map OliveDrab4 105-139-34 + tcl::dict::set TK_colour_map orange 255-165-0 + tcl::dict::set TK_colour_map "orange red" 255-69-0 + tcl::dict::set TK_colour_map orange1 255-165-0 + tcl::dict::set TK_colour_map orange2 238-154-0 + tcl::dict::set TK_colour_map orange3 205-133-0 + tcl::dict::set TK_colour_map orange4 139-90-0 + tcl::dict::set TK_colour_map OrangeRed 255-69-0 + tcl::dict::set TK_colour_map OrangeRed1 255-69-0 + tcl::dict::set TK_colour_map OrangeRed2 238-64-0 + tcl::dict::set TK_colour_map OrangeRed3 205-55-0 + tcl::dict::set TK_colour_map OrangeRed4 139-37-0 + tcl::dict::set TK_colour_map orchid 218-112-214 + tcl::dict::set TK_colour_map orchid1 255-131-250 + tcl::dict::set TK_colour_map orchid2 238-122-233 + tcl::dict::set TK_colour_map orchid3 205-105-201 + tcl::dict::set TK_colour_map orchid4 139-71-137 + tcl::dict::set TK_colour_map "pale goldenrod" 238-232-170 + tcl::dict::set TK_colour_map "pale green" 152-251-152 + tcl::dict::set TK_colour_map "pale turquoise" 175-238-238 + tcl::dict::set TK_colour_map "pale violet red" 219-112-147 + tcl::dict::set TK_colour_map PaleGoldenrod 238-232-170 + tcl::dict::set TK_colour_map PaleGreen 152-251-152 + tcl::dict::set TK_colour_map PaleGreen1 154-255-154 + tcl::dict::set TK_colour_map PaleGreen2 144-238-144 + tcl::dict::set TK_colour_map PaleGreen3 124-205-124 + tcl::dict::set TK_colour_map PaleGreen4 84-139-84 + tcl::dict::set TK_colour_map PaleTurquoise 175-238-238 + tcl::dict::set TK_colour_map PaleTurquoise1 187-255-255 + tcl::dict::set TK_colour_map PaleTurquoise2 174-238-238 + tcl::dict::set TK_colour_map PaleTurquoise3 150-205-205 + tcl::dict::set TK_colour_map PaleTurquoise4 102-139-139 + tcl::dict::set TK_colour_map PaleVioletRed 219-112-147 + tcl::dict::set TK_colour_map PaleVioletRed1 255-130-171 + tcl::dict::set TK_colour_map PaleVioletRed2 238-121-159 + tcl::dict::set TK_colour_map PaleVioletRed3 205-104-127 + tcl::dict::set TK_colour_map PaleVioletRed4 139-71-93 + tcl::dict::set TK_colour_map "papaya whip" 255-239-213 + tcl::dict::set TK_colour_map PapayaWhip 255-239-213 + tcl::dict::set TK_colour_map "peach puff" 255-218-185 + tcl::dict::set TK_colour_map PeachPuff 255-218-185 + tcl::dict::set TK_colour_map PeachPuff1 255-218-185 + tcl::dict::set TK_colour_map PeachPuff2 238-203-173 + tcl::dict::set TK_colour_map PeachPuff3 205-175-149 + tcl::dict::set TK_colour_map PeachPuff4 139-119-101 + tcl::dict::set TK_colour_map peru 205-133-63 + tcl::dict::set TK_colour_map pink 255-192-203 + tcl::dict::set TK_colour_map pink1 255-181-197 + tcl::dict::set TK_colour_map pink2 238-169-184 + tcl::dict::set TK_colour_map pink3 205-145-158 + tcl::dict::set TK_colour_map pink4 139-99-108 + tcl::dict::set TK_colour_map plum 221-160-221 + tcl::dict::set TK_colour_map plum1 255-187-255 + tcl::dict::set TK_colour_map plum2 238-174-238 + tcl::dict::set TK_colour_map plum3 205-150-205 + tcl::dict::set TK_colour_map plum4 139-102-139 + tcl::dict::set TK_colour_map "powder blue" 176-224-230 + tcl::dict::set TK_colour_map PowderBlue 176-224-230 + tcl::dict::set TK_colour_map purple 128-0-128 + tcl::dict::set TK_colour_map purple1 155-48-255 + tcl::dict::set TK_colour_map purple2 145-44-238 + tcl::dict::set TK_colour_map purple3 125-38-205 + tcl::dict::set TK_colour_map purple4 85-26-139 + tcl::dict::set TK_colour_map red 255-0-0 + tcl::dict::set TK_colour_map red1 255-0-0 + tcl::dict::set TK_colour_map red2 238-0-0 + tcl::dict::set TK_colour_map red3 205-0-0 + tcl::dict::set TK_colour_map red4 139-0-0 + tcl::dict::set TK_colour_map "rosy brown" 188-143-143 + tcl::dict::set TK_colour_map RosyBrown 188-143-143 + tcl::dict::set TK_colour_map RosyBrown1 255-193-193 + tcl::dict::set TK_colour_map RosyBrown2 238-180-180 + tcl::dict::set TK_colour_map RosyBrown3 205-155-155 + tcl::dict::set TK_colour_map RosyBrown4 139-105-105 + tcl::dict::set TK_colour_map "royal blue" 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue1 72-118-255 + tcl::dict::set TK_colour_map RoyalBlue2 67-110-238 + tcl::dict::set TK_colour_map RoyalBlue3 58-95-205 + tcl::dict::set TK_colour_map RoyalBlue4 39-64-139 + tcl::dict::set TK_colour_map "saddle brown" 139-69-19 + tcl::dict::set TK_colour_map SaddleBrown 139-69-19 + tcl::dict::set TK_colour_map salmon 250-128-114 + tcl::dict::set TK_colour_map salmon1 255-140-105 + tcl::dict::set TK_colour_map salmon2 238-130-98 + tcl::dict::set TK_colour_map salmon3 205-112-84 + tcl::dict::set TK_colour_map salmon4 139-76-57 + tcl::dict::set TK_colour_map "sandy brown" 244-164-96 + tcl::dict::set TK_colour_map SandyBrown 244-164-96 + tcl::dict::set TK_colour_map "sea green" 46-139-87 + tcl::dict::set TK_colour_map SeaGreen 46-139-87 + tcl::dict::set TK_colour_map SeaGreen1 84-255-159 + tcl::dict::set TK_colour_map SeaGreen2 78-238-148 + tcl::dict::set TK_colour_map SeaGreen3 67-205-128 + tcl::dict::set TK_colour_map SeaGreen4 46-139-87 + tcl::dict::set TK_colour_map seashell 255-245-238 + tcl::dict::set TK_colour_map seashell1 255-245-238 + tcl::dict::set TK_colour_map seashell2 238-229-222 + tcl::dict::set TK_colour_map seashell3 205-197-191 + tcl::dict::set TK_colour_map seashell4 139-134-130 + tcl::dict::set TK_colour_map sienna 160-82-45 + tcl::dict::set TK_colour_map sienna1 255-130-71 + tcl::dict::set TK_colour_map sienna2 238-121-66 + tcl::dict::set TK_colour_map sienna3 205-104-57 + tcl::dict::set TK_colour_map sienna4 139-71-38 + tcl::dict::set TK_colour_map silver 192-192-192 + tcl::dict::set TK_colour_map "sky blue" 135-206-235 + tcl::dict::set TK_colour_map SkyBlue 135-206-235 + tcl::dict::set TK_colour_map SkyBlue1 135-206-255 + tcl::dict::set TK_colour_map SkyBlue2 126-192-238 + tcl::dict::set TK_colour_map SkyBlue3 108-166-205 + tcl::dict::set TK_colour_map SkyBlue4 74-112-139 + tcl::dict::set TK_colour_map "slate blue" 106-90-205 + tcl::dict::set TK_colour_map "slate gray" 112-128-144 + tcl::dict::set TK_colour_map "slate grey" 112-128-144 + tcl::dict::set TK_colour_map SlateBlue 106-90-205 + tcl::dict::set TK_colour_map SlateBlue1 131-111-255 + tcl::dict::set TK_colour_map SlateBlue2 122-103-238 + tcl::dict::set TK_colour_map SlateBlue3 105-89-205 + tcl::dict::set TK_colour_map SlateBlue4 71-60-139 + tcl::dict::set TK_colour_map SlateGray 112-128-144 + tcl::dict::set TK_colour_map SlateGray1 198-226-255 + tcl::dict::set TK_colour_map SlateGray2 185-211-238 + tcl::dict::set TK_colour_map SlateGray3 159-182-205 + tcl::dict::set TK_colour_map SlateGray4 108-123-139 + tcl::dict::set TK_colour_map SlateGrey 112-128-144 + tcl::dict::set TK_colour_map snow 255-250-250 + tcl::dict::set TK_colour_map snow1 255-250-250 + tcl::dict::set TK_colour_map snow2 238-233-233 + tcl::dict::set TK_colour_map snow3 205-201-201 + tcl::dict::set TK_colour_map snow4 139-137-137 + tcl::dict::set TK_colour_map "spring green" 0-255-127 + tcl::dict::set TK_colour_map SpringGreen 0-255-127 + tcl::dict::set TK_colour_map SpringGreen1 0-255-127 + tcl::dict::set TK_colour_map SpringGreen2 0-238-118 + tcl::dict::set TK_colour_map SpringGreen3 0-205-102 + tcl::dict::set TK_colour_map SpringGreen4 0-139-69 + tcl::dict::set TK_colour_map "steel blue" 70-130-180 + tcl::dict::set TK_colour_map SteelBlue 70-130-180 + tcl::dict::set TK_colour_map SteelBlue1 99-184-255 + tcl::dict::set TK_colour_map SteelBlue2 92-172-238 + tcl::dict::set TK_colour_map SteelBlue3 79-148-205 + tcl::dict::set TK_colour_map SteelBlue4 54-100-139 + tcl::dict::set TK_colour_map tan 210-180-140 + tcl::dict::set TK_colour_map tan1 255-165-79 + tcl::dict::set TK_colour_map tan2 238-154-73 + tcl::dict::set TK_colour_map tan3 205-133-63 + tcl::dict::set TK_colour_map tan4 139-90-43 + tcl::dict::set TK_colour_map teal 0-128-128 + tcl::dict::set TK_colour_map thistle 216-191-216 + tcl::dict::set TK_colour_map thistle1 255-225-255 + tcl::dict::set TK_colour_map thistle2 238-210-238 + tcl::dict::set TK_colour_map thistle3 205-181-205 + tcl::dict::set TK_colour_map thistle4 139-123-139 + tcl::dict::set TK_colour_map tomato 255-99-71 + tcl::dict::set TK_colour_map tomato1 255-99-71 + tcl::dict::set TK_colour_map tomato2 238-92-66 + tcl::dict::set TK_colour_map tomato3 205-79-57 + tcl::dict::set TK_colour_map tomato4 139-54-38 + tcl::dict::set TK_colour_map turquoise 64-224-208 + tcl::dict::set TK_colour_map turquoise1 0-245-255 + tcl::dict::set TK_colour_map turquoise2 0-229-238 + tcl::dict::set TK_colour_map turquoise3 0-197-205 + tcl::dict::set TK_colour_map turquoise4 0-134-139 + tcl::dict::set TK_colour_map violet 238-130-238 + tcl::dict::set TK_colour_map "violet red" 208-32-144 + tcl::dict::set TK_colour_map VioletRed 208-32-144 + tcl::dict::set TK_colour_map VioletRed1 255-62-150 + tcl::dict::set TK_colour_map VioletRed2 238-58-140 + tcl::dict::set TK_colour_map VioletRed3 205-50-120 + tcl::dict::set TK_colour_map VioletRed4 139-34-82 + tcl::dict::set TK_colour_map wheat 245-222-179 + tcl::dict::set TK_colour_map wheat1 255-231-186 + tcl::dict::set TK_colour_map wheat2 238-216-174 + tcl::dict::set TK_colour_map wheat3 205-186-150 + tcl::dict::set TK_colour_map wheat4 139-126-102 + tcl::dict::set TK_colour_map white 255-255-255 + tcl::dict::set TK_colour_map "white smoke" 245-245-245 + tcl::dict::set TK_colour_map WhiteSmoke 245-245-245 + tcl::dict::set TK_colour_map yellow 255-255-0 + tcl::dict::set TK_colour_map "yellow green" 154-205-50 + tcl::dict::set TK_colour_map yellow1 255-255-0 + tcl::dict::set TK_colour_map yellow2 238-238-0 + tcl::dict::set TK_colour_map yellow3 205-205-0 + tcl::dict::set TK_colour_map yellow4 139-139-0 + tcl::dict::set TK_colour_map YellowGreen 154-205-50 + + variable TK_colour_map_lookup ;#same dict but with lower-case versions added + set TK_colour_map_lookup $TK_colour_map + dict for {key val} $TK_colour_map { + dict set TK_colour_map_lookup [tcl::string::tolower $key] $val ;#no need to test if already present - just set. + } + + variable TK_colour_map_reverse [dict create] + dict for {key val} $TK_colour_map { + dict lappend TK_colour_map_reverse $val $key + } + + #using same order as inital colour map + variable TK_colour_map_merge [dict create] + set seen_names [dict create] + dict for {key val} $TK_colour_map { + if {[dict exists $seen_names $key]} { + continue + } + set allnames [dict get $TK_colour_map_reverse $val] + set names [list] + foreach n $allnames { + if {$n ne $key} { + lappend names $n + } + } + dict set TK_colour_map_merge $key [dict create colour $val names $names] + foreach n $names { + dict set seen_names $n 1 + } + } + unset seen_names + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval ::punk::ansi::colourmap::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +# variable PUNKARGS +# variable PUNKARGS_aliases +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::ansi::colourmap +} +# ----------------------------------------------------------------------------- + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::ansi::colourmap [tcl::namespace::eval ::punk::ansi::colourmap { + variable pkg ::punk::ansi::colourmap + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/punk/args-0.1.1.tm b/src/bootsupport/modules/punk/args-0.1.1.tm deleted file mode 100644 index 2d8de97d..00000000 --- a/src/bootsupport/modules/punk/args-0.1.1.tm +++ /dev/null @@ -1,5341 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.0 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - return -code error -errorcode {TCL WRONGARGS PUNK} $result - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define" - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance - #todo - configurable per interp/namespace - -errorstyle -type string -default enhanced -choices {enhanced standard minimal} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - set opts [dict merge $opts $defaultopts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg opts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname]] $msg - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - ##try trap? - ##return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - ##throw ? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname]] $msg - #arg_error $msg $argspecs -badarg $argname - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $::errorCode] $::errorInfo - } - standard { - puts stderr "(todo enhanced error) PUNKARGS VALIDATION: $msg\n$opts" - } - enhanced { - puts stderr "(todo enhanced error) PUNKARGS VALIDATION: $msg\n$opts" - } - } - return - } trap {PUNKARGS} {msg opts} { - #trap punk::args argument validation/parsing errors and decide here - #whether to display basic error - or full usage if configured. - puts stderr "PUNKARGS OTHER: $msg\n$opts" - #JJJ - return - } trap {} {msg opts} { - #review - #puts stderr "$msg\n$opts" - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $opts -errorcode] [dict get $opts -errorinfo] - return - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lapend solosreceived $fullopt - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for [Get_caller]. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for [Get_caller]. Unexpected option \"$a\": No options defined while @opts -any 0" - } - arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for [Get_caller] requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for [Get_caller] requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for [Get_caller] didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - arg_error "Option $argname for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname]] $msg - arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.1 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/args-0.1.4.tm b/src/bootsupport/modules/punk/args-0.1.4.tm deleted file mode 100644 index e1256fe4..00000000 --- a/src/bootsupport/modules/punk/args-0.1.4.tm +++ /dev/null @@ -1,5502 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.4 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.4] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/args-0.1.6.tm b/src/bootsupport/modules/punk/args-0.1.6.tm deleted file mode 100644 index c3bf04b8..00000000 --- a/src/bootsupport/modules/punk/args-0.1.6.tm +++ /dev/null @@ -1,6400 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.6 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.6] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - directive-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - directive-options: -any - %B%@values%N% ?opt val...? - directive-options: -min -max - (used for trailing args that come after switches/opts) - %B%@form%N% ?opt val...? - directive-options: -form -synopsis - (used for commands with multiple forms) - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VALSPEC_DEFAULTS $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple - - -prefix { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -form -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - literal { - #value is the name of the argument - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -type literal - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS]" - dict set resultdict $type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} { - return - } - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(goodarg) [a+ green strike] - set CLR(goodchoice) [a+ reverse] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(goodarg) [a+ strike] - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - #if {"$argdisplay_header$argdisplay_body" eq ""} { - # set is_custom_argdisplay 0 - #} else { - # set is_custom_argdisplay 1 - #} - - #temp - TODO - set argdisplay_header "" - set argdisplay_body "" - set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $form_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $form_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $c] - if {[dict get $arginfo -prefix]} { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - } else { - lappend opt_names_display $c - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - punk::args::parse $args withid ::punk::args::parse - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" - } - } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - set VAL_MIN 0 - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$VAL_MIN ne ""} { - if {[llength $rawargs] > $VAL_MIN} { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - #JJJJ - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - set valmax -1 - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ne $fullopt} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $rawargs 0 $i-1] - #set post_values [lrange $rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$OPT_ANY} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - #--------------------------------------- - set ordered_opts [dict create] - foreach o $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $o]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $o] - } - } - #add in possible '-any true' opts after the defined opts - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - #---------------------------------------- - #set leaders_dict $LEADER_DEFAULTS ;#wrong - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #!!! review - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - incr nameidx - set val [lindex $values $validx] - if {$valname ne ""} { - if {[llength $valname] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname', but requires [llength $valname] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $strideval] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $strideval - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $strideval - } - lappend valnames_received $valname - } else { - if {$valname_multiple ne ""} { - if {[llength $valname_multiple] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname_multiple { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname_multiple', but requires [llength $valname_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname_multiple] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $strideval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - set positionalidx [expr {$start_position + $validx}] - } - #------------------------------------------ - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $OPT_REQUIRED $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - literal { - foreach e $vlist { - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - set display "?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "?[lindex [dict get $arginfo -choices] 0]?" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display "?$argname?" - } else { - set display "?$I$argname$RST?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$I$argname$RST ?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "[lindex [dict get $arginfo -choices] 0]" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display "$I$argname$RST" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.6 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/args-0.1.7.tm b/src/bootsupport/modules/punk/args-0.1.7.tm deleted file mode 100644 index b04f4966..00000000 --- a/src/bootsupport/modules/punk/args-0.1.7.tm +++ /dev/null @@ -1,6458 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.7 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.7] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - directive-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - directive-options: -any - %B%@values%N% ?opt val...? - directive-options: -min -max - (used for trailing args that come after switches/opts) - %B%@form%N% ?opt val...? - directive-options: -form -synopsis - (used for commands with multiple forms) - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VALSPEC_DEFAULTS $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache [list $optionspecs]]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache [list $optionspecs]] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple - - -prefix { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -form -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - literal { - #value is the name of the argument - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -type literal - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache [list $optionspecs] $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS]" - dict set resultdict $type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$deflist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set deflist [raw_def $id] - if {$deflist eq ""} { - return - } - return [resolve {*}$deflist] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - variable arg_error_CLR - array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] - variable arg_error_CLR_nocolour - array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" - variable arg_error_CLR_info - array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error - array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] - - - #bas ic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR - - switch -- $scheme { - nocolour { - variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour - } - info { - variable arg_error_CLR_info - array set CLR [array get arg_error_CLR_info] - } - error { - variable arg_error_CLR_error - array set CLR [array get arg_error_CLR_error] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - #if {"$argdisplay_header$argdisplay_body" eq ""} { - # set is_custom_argdisplay 0 - #} else { - # set is_custom_argdisplay 1 - #} - - #temp - TODO - set argdisplay_header "" - set argdisplay_body "" - set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $form_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $form_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $c] - if {[dict get $arginfo -prefix]} { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - } else { - lappend opt_names_display $c - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - #unhappy path - not enough options - #review - which form of punk::args::parse? - punk::args::parse $args withid ::punk::args::parse - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" - } - } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - set VAL_MIN 0 - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$VAL_MIN ne ""} { - if {[llength $rawargs] > $VAL_MIN} { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - #JJJJ - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - set valmax -1 - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ne $fullopt} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $rawargs 0 $i-1] - #set post_values [lrange $rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$OPT_ANY} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - #--------------------------------------- - set ordered_opts [dict create] - foreach o $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $o]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $o] - } - } - #add in possible '-any true' opts after the defined opts - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - #---------------------------------------- - #set leaders_dict $LEADER_DEFAULTS ;#wrong - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #!!! review - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - incr nameidx - set val [lindex $values $validx] - if {$valname ne ""} { - if {[llength $valname] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname', but requires [llength $valname] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $strideval] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $strideval - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $strideval - } - lappend valnames_received $valname - } else { - if {$valname_multiple ne ""} { - if {[llength $valname_multiple] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname_multiple { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname_multiple', but requires [llength $valname_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname_multiple] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $strideval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - set positionalidx [expr {$start_position + $validx}] - } - #------------------------------------------ - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $OPT_REQUIRED $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - literal { - foreach e $vlist { - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - lassign {} low high ;#set both empty - set has_range 0 - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - set has_range 1 - } - } - foreach e $vlist e_check $vlist_check { - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$has_range} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - set display "?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "?[lindex [dict get $arginfo -choices] 0]?" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display "?$argname?" - } else { - set display "?$I$argname$RST?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$I$argname$RST ?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "[lindex [dict get $arginfo -choices] 0]" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display "$I$argname$RST" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.7 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/bootsupport/modules/punk/args-0.1.8.tm b/src/bootsupport/modules/punk/args-0.1.8.tm deleted file mode 100644 index c17ecc2c..00000000 --- a/src/bootsupport/modules/punk/args-0.1.8.tm +++ /dev/null @@ -1,7213 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.8 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.8] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %G% \x1b\[32m %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - (used for leading args that come before switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) - %B%@opts%N% ?opt val...? - directive-options: -any|-arbitrary - %B%@values%N% ?opt val...? - (used for trailing args that come after switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) - %B%@form%N% ?opt val...? - (used for commands with multiple forms) - directive-options: -form -synopsis - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - int|integer - number - list - indexexpression - dict - double - bool|boolean - char - file - directory - ansistring - globstring - (any of the types accepted by 'string is') - - The above all perform some validation checks - - string - (also any of the 'string is' types such as - xdigit, graph, punct, lower etc) - any - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choiceprefixreservelist {} - These choices are additional values used in prefix calculation. - The values will not be added to the list of available choices. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - {Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - ${[punk::args::tclcore::argdoc::example { - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\ - "Description of command" - - %G%#The following option defines an option-value pair%R% - %G%#It may have aliases by separating them with a pipe |%R% - -fg|-foreground -default blah -type string -help\ - "In the result dict returned by punk::args::parse - the value used in the opts key will always be the last - entry, in this case -foreground" - %G%#The following option defines a flag style option (solo)%R% - -flag1 -default 0 -type none -help\ - "Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars" - - @values -min 1 -max -1 - %G%#Items that don't begin with * or - are value definitions%R% - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } "@doc -name Manpage: -url [myfunc_manpage_geturl myns::myfunc]"}]} - } - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADER_UNNAMED false\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_MIN ""\ - OPT_MAX ""\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VAL_UNNAMED false\ - VALSPEC_DEFAULTS $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - #set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -paramindents none -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache [list $optionspecs]]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache [list $optionspecs]] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - -arbitrary - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -min { - dict set F $fid OPT_MIN $v - } - -max { - dict set F $fid OPT_MAX $v - } - -minsize - -maxsize - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -validationtransform { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple - - -prefix { - #check is bool - if {![string is boolean -strict $v]} { - error "punk::args::define - Option '$k' has value '$v'of wrong type in @opts line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -arbitrary -form -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -choiceprefix - - -choicerestricted { - if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -minsize - -maxsize - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefixdenylist - -choiceprefixreservelist - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo - -choicelabels { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid LEADER_UNNAMED $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -multiple { - if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform { - tcl::dict::set tmp_valspec_defaults $k $v - } - -unnamed { - if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" - } - dict set F $fid VAL_UNNAMED $v - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ - -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -unnamed\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #todo - could be a list e.g {any int literal(Test)} - #case must be preserved in literal bracketed part - set typelist [list] - foreach typespec $specval { - set lc_typespec [tcl::string::tolower $typespec] - #normalize here so we don't have to test during actual args parsing in main function - switch -- $lc_typespec { - int - integer { - lappend typelist int - } - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - lappend typelist double - } - bool - boolean { - lappend typelist bool - } - char - character { - lappend typelist char - } - dict - dictionary { - lappend typelist dict - } - index - indexexpression { - lappend typelist indexexpression - } - "" - none { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $typelist] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::define - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - lappend typelist none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" - } - } - any - anything { - lappend typelist any - } - ansi - ansistring { - lappend typelist ansistring - } - string - globstring { - lappend typelist $lc_typespec - } - literal { - #value is the name of the argument - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" - } - lappend typelist literal - } - default { - if {[string match literal* $lc_typespec]} { - set literal_tail [string range $typespec 7 end] - lappend typelist literal$literal_tail - } else { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - lappend typelist $lc_typespec - } - } - } - } - tcl::dict::set spec_merged -type $typelist - } - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -default { - tcl::dict::set spec_merged -default $specval - if {![dict exists $argdef_values -optional]} { - tcl::dict::set spec_merged -optional 1 - } - } - -optional { - tcl::dict::set spec_merged -optional $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups\ - -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - #if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} {} - if {![tcl::dict::get $spec_merged -optional]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - - - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache [list $optionspecs] $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - #set fullk [tcl::prefix::match -error "" {-return -form -types -antiglobs -override} $k] - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "\"$m\" [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS]" - dict set resultdict $type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "\"$m\" [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "\"$m\" $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$deflist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set deflist [raw_def $id] - if {$deflist eq ""} { - return - } - return [resolve {*}$deflist] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - variable arg_error_CLR - array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] - variable arg_error_CLR_nocolour - array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" - variable arg_error_CLR_info - array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error - array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] - - - #bas ic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR - - switch -- $scheme { - nocolour { - variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour - } - info { - variable arg_error_CLR_info - array set CLR [array get arg_error_CLR_info] - } - error { - variable arg_error_CLR_error - array set CLR [array get arg_error_CLR_error] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - #if {"$argdisplay_header$argdisplay_body" eq ""} { - # set is_custom_argdisplay 0 - #} else { - # set is_custom_argdisplay 1 - #} - - #temp - TODO - set argdisplay_header "" - set argdisplay_body "" - set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - set lookup_optset [dict create] - if {[llength [dict get $form_dict OPT_NAMES]]} { - set all_opts [list] - foreach optset [dict get $form_dict OPT_NAMES] { - set optmembers [split $optset |] - lappend all_opts {*}$optmembers - foreach o $optmembers { - dict set lookup_optset $o $optset - #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 - if {![catch {package require punk::trie}]} { - #todo - reservelist for future options - or just to affect the prefix calculation - # (similar to -choiceprefixreservelist) - - set trie [punk::trie::trieclass new {*}$all_opts --] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach optset [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $optset] - if {[dict get $arginfo -prefix]} { - set opt_members [split $optset |] - set odisplay [list] - foreach opt $opt_members { - set id [dict get $idents $opt] - #REVIEW - if {$id eq $opt} { - set prefix $opt - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $opt 0 $idlen-1] - set tail [string range $opt $idlen end] - } - lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail - } - #lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - lappend opt_names_display [join $odisplay |] - } else { - lappend opt_names_display $optset - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $optset - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 $parsed_leaders] [list opts $opt_names_display $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $arginfo -choiceprefixreservelist {}] ;#names used to calc prefix - but not available as actual choice. - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - #review - does choiceprefixdenylist need to be added? - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_prefixcalc [list {*}[string tolower $allchoices_originalcase] {*}$choiceprefixreservelist] - } else { - set casemsg " (case sensitive)" - set allchoices_prefixcalc [list {*}$allchoices_originalcase {*}$choiceprefixreservelist] - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_prefixcalc] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - - # ------------------------------------------------------------------------------------------------------- - # if the argument class can accept unnamed arguments (or if opts accepts unspecified flags) - display an indication - # ------------------------------------------------------------------------------------------------------- - switch -- $argumentclass { - leaders - values { - if {$argumentclass eq "leaders"} { - set class_unnamed LEADER_UNNAMED - set class_max LEADER_MAX - set class_required LEADER_REQUIRED - set class_directive_defaults LEADERSPEC_DEFAULTS - } else { - set class_unnamed VAL_UNNAMED - set class_max VAL_MAX - set class_required VAL_REQUIRED - set class_directive_defaults VALSPEC_DEFAULTS - } - if {[dict get $form_dict $class_unnamed]} { - set valmax [dict get $form_dict $class_max] - #set valmin [dict get $form_dict VAL_MIN] - if {$valmax eq ""} { - set valmax -1 - } - if {$valmax == -1} { - set possible_unnamed -1 - } else { - set possible_unnamed [expr {$valmax - [llength [dict get $form_dict $class_required]]}] - if {$possible_unnamed < 0} { - set possible_unnamed 0 - } - } - if {$possible_unnamed == -1 || $possible_unnamed > 0} { - #Note 'multiple' is always empty here as each unnamed is assigned to its own positional index - if {$possible_unnamed == 1} { - set argshow ?? - } else { - set argshow ?...? - } - set tp [dict get $form_dict $class_directive_defaults -type] - if {[dict exists $form_dict $class_directive_defaults -default]} { - set default [dict get $form_dict $class_directive_defaults -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - opts { - #display row to indicate if -any|-arbitrary true - - #review OPTSPEC_DEFAULTS -multiple ? - if {[dict get $form_dict OPT_ANY]} { - set argshow "?...?" - set tp [dict get $form_dict OPTSPEC_DEFAULTS -type] - if {[dict exists $form_dict OPTSPEC_DEFAULTS -default]} { - set default [dict get $form_dict OPTSPEC_DEFAULTS -default] - } else { - set default "" - } - if {$use_table} { - $t add_row [list "$argshow" $tp $default "" ""] - } else { - set arghelp "[a+ bold]$argshow$RST TYPE:$tp DEFAULT:$default\n" - lappend errlines $arghelp - } - } - } - } - - } ;#end foreach argumentclass - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - catch {$t destroy} - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - #puts "punk::args::parse --> '$args'" - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - #unhappy path - not enough options - #review - which form of punk::args::parse? - punk::args::parse $args withid ::punk::args::parse - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - #puts stdout "punk::args::parse '$parseargs' withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - #puts stdout "punk::args::parse '$parseargs' with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - #puts stdout "parse --> get_dict $parseargs -form [dict get $opts -form]" - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - - #return number of values we can assign to cater for variable length clauses such as {"elseif" expr "?then?" body} - #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { - set ARG_INFO [dict get $formdict ARG_INFO] - set all_remaining [lrange $values $idx end] - set thisname [lindex $names $nameidx] - set thistype [dict get $ARG_INFO $thisname -type] - set tailnames [lrange $names $nameidx+1 end] - - #todo - work backwards with any (optional or not) literals at tail that match our values - and remove from assignability. - set ridx 0 - foreach clausename [lreverse $tailnames] { - #puts "=============== clausename:$clausename all_remaining: $all_remaining" - set typelist [dict get $ARG_INFO $clausename -type] - if {[lsearch $typelist literal*] == -1} { - break - } - set max_clause_length [llength $typelist] - if {$max_clause_length == 1} { - #basic case - set alloc_ok 0 - #set v [lindex $values end-$ridx] - set v [lindex $all_remaining end] - set tp [lindex $typelist 0] - #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole name should be marked -optional true instead. - set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #plain "literal" without bracketed specifier - match to argument name - set match $clausename - } - if {$v eq $match} { - set alloc_ok 1 - lpop all_remaining - if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames - } - } else { - #break - } - } else { - #break - } - if {!$alloc_ok} { - if {![dict get $ARG_INFO $clausename -optional]} { - break - } - } - } else { - #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) - #This is better caught during definition. - #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} - #set cvals [lrange $values end-$ridx end-[expr {$ridx + $max_clause_length-1}]] - set cvals [lrange $values end-[expr {$ridx + $max_clause_length-1}] end-$ridx] - set rcvals [lreverse $cvals] - set alloc_count 0 - #clause name may have more entries than types - extras at beginning are ignored - set rtypelist [lreverse $typelist] - set rclausename [lrange [lreverse $clausename] 0 [llength $typelist]-1] - #assert length of rtypelist >= $rclausename - set alloc_ok 0 - set reverse_type_index 0 - foreach tp $rtypelist membername $rclausename { - #(membername may be empty if not enough elements) - #set rv [lindex $rcvals end-$alloc_count] - set rv [lindex $all_remaining end-$alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #if membername empty - equivalent to "literal()" - matches empty string literal - #edgecase - possibly? no need for empty-string literals - but allow it without error. - set match $membername - } - if {$rv eq $match} { - set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok - incr alloc_count - } else { - if {$clause_member_optional} { - # - } else { - set alloc_ok 0 - break - } - } - } else { - if {$clause_member_optional} { - #review - optional non-literal makes things harder.. - #we don't want to do full type checking here - but we now risk allocating an item that should actually - #be allocated to the previous value - set prev_type [lindex $rtypelist $reverse_type_index+1] - if {[string match literal* $prev_type]} { - set litinfo [string range $prev_type 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #prev membername - set match [lindex $rclausename $reverse_type_index+1] - } - if {$rv ne $match} { - #current val doesn't match previous type - allocate here - incr alloc_count - } - } else { - #no literal to anchor against.. - incr alloc_count - } - } else { - #allocate regardless of type - we're only matching on arity and literal positioning here. - #leave final type-checking for later. - incr alloc_count - } - } - incr reverse_type_index - } - if {$alloc_ok && $alloc_count > 0} { - #set n [expr {$alloc_count -1}] - #set all_remaining [lrange $all_remaining end-$n end] - set all_remaining [lrange $all_remaining 0 end-$alloc_count] - #don't lpop if -multiple true - if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames - } - } else { - break - } - } - incr ridx - } - set num_remaining [llength $all_remaining] - - if {[dict get $ARG_INFO $thisname -optional] || ([dict get $ARG_INFO $thisname -multiple] && $thisname in $namesreceived)} { - #todo - check -multiple for required min/max (not implemented: make -multiple accept ?) - #thisname already satisfied, or not required - set tail_needs 0 - foreach t $tailnames { - if {![dict get $ARG_INFO $t -optional]} { - set min_clause_length [llength [lsearch -all -not [dict get $ARG_INFO $t -type] {\?*\?}]] - incr tail_needs $min_clause_length - } - } - set all_remaining [lrange $all_remaining 0 end-$tail_needs] - } - - #thistype - set alloc_ok 1 - set alloc_count 0 - set resultlist [list] - set n [expr {[llength $thistype]-1}] - #name can have more or less items than typelist - set thisnametail [lrange $thisname end-$n end] - foreach tp $thistype membername $thisnametail { - set v [lindex $all_remaining $alloc_count] - if {[string match {\?*\?} $tp]} { - set clause_member_optional 1 - } else { - set clause_member_optional 0 - } - set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - set match $membername - } - if {$v eq $match} { - if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { - lappend resultlist "" - } else { - lappend resultlist $v - incr alloc_count - } - } else { - if {$clause_member_optional} { - #todo - configurable default for optional clause members? - lappend resultlist "" - } else { - set alloc_ok 0 - break - } - } - } else { - if {$clause_member_optional} { - if {$alloc_count >= [llength $all_remaining]} { - lappend resultlist "" - } else { - lappend resultlist $v - incr alloc_count - } - } else { - lappend resultlist $v - incr alloc_count - } - } - if {$alloc_count > [llength $all_remaining]} { - set alloc_ok 0 - break - } - } - if {$alloc_ok} { - set d [dict create consumed $alloc_count resultlist $resultlist] - } else { - set d [dict create consumed 0 resultlist {}] - } - #puts ">>>> _get_dict_can_assign_value $d" - return $d - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" - } - } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - #set VAL_MIN 0 - foreach v $VAL_NAMES { - if {![dict get $ARG_INFO $v -optional]} { - # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) - # e.g -types {a ?xxx?} - #this has one required and one optional - set typelist [dict get $ARG_INFO $v -type] - set clause_length 0 - foreach t $typelist { - if {![string match {\?*\?} $t]} { - incr clause_length - } - } - incr valmin $clause_length - } - } - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - #set optnames [lsearch -all -inline $argnames -*] - #JJJ - set all_opts [list] - set lookup_optset [dict create] - foreach optset $OPT_NAMES { - set optmembers [split $optset |] - lappend all_opts {*}$optmembers - foreach opt $optmembers { - dict set lookup_optset $opt $optset - } - } - set ridx 0 - set rawargs_copy $rawargs - set remaining_rawargs $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - #consider for example: LEADER_NAMES {"k v" "a b c" x} - #(i.e clause-length of 2 3 and 1) - #This will take 6 raw leaders to fill in the basic case that all are -optional 0 and -multiple 0 - set named_leader_args_max 0 - foreach ln $LEADER_NAMES { - incr named_leader_args_max [llength $ln] - } - - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} - set nameidx 0 - if {$LEADER_MAX != 0} { - for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { - set r [lindex $rawargs $ridx] - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $nameidx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > $named_leader_args_max-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string - } - if {$OPT_MAX ne "0" && $r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {$OPT_MAX ne "0" && [tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $all_opts $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - if {$leader_posn_name ne ""} { - #false alarm - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - #incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" - set clauselength [llength $leader_posn_name] - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $remaining_rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop remaining_rawargs 0] - # incr ridx - # continue - # } - #} - if {[llength $remaining_rawargs] < $clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - #check if enough remaining_rawargs to fill any required values - if {$valmin > 0 && [llength $remaining_rawargs] - $clauselength < $valmin} { - break - } - - #leadername may be a 'clause' of arbitrary length (e.g {"key val"} or {"key val etc"}) - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {[llength $remaining_rawargs] < $clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break - } - - if {$valmin > 0 && [llength $remaining_rawargs] - $clauselength < $valmin} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values - #we still need to check if enough values for the leader itself - if {[llength $remaining_rawargs] < $clauselength} { - #not enough remaining args to fill *required* leader - break - } - - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$valmin > 0} { - if {[llength $remaining_rawargs] > $valmin} { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #review - if is_multiple, keep going if enough remaining_rawargs for values? - break - } - } - - #incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - set valmax -1 - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - remaining_rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here -#JJJ - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> pre_values: $pre_values" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "remaining_rawargs: $remaining_rawargs" - #puts stderr "argstate: $argstate" - if {$OPT_MAX ne "0" && [lsearch $remaining_rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $remaining_rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $remaining_rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $remaining_rawargs $i] - set remaining_args_including_this [expr {[llength $remaining_rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $remaining_rawargs 0 $i] - set post_values [lrange $remaining_rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $remaining_rawargs 0 $i] - set post_values [lrange $remaining_rawargs $i+1 end] - } - break - } else { - set opt [tcl::prefix match -error "" [list {*}$all_opts --] $a] - if {$opt eq "--"} {set opt ""} - if {[dict exists $lookup_optset $opt]} { - set fullopt [dict get $lookup_optset $opt] - } else { - set fullopt "" - } - if {$fullopt ne ""} { - #e.g when fullopt eq -fg|-foreground - #-fg is an alias , -foreground is the 'api' value for the result dict - #$fullopt remains as the key in the spec - set optmembers [split $fullopt |] - set api_opt [lindex $optmembers end] - - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ni $optmembers} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $all_opts" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $all_opts] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $remaining_rawargs 0 $i-1] - #set post_values [lrange $remaining_rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - - set flagval [lindex $remaining_rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$api_opt ni $flagsreceived} { - tcl::dict::set opts $api_opt [list $flagval] - } else { - tcl::dict::lappend opts $api_opt $flagval - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - tcl::dict::set opts $api_opt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $api_opt 1 - } else { - tcl::dict::lappend opts $api_opt 1 - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $api_opt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok - } - lappend flagsreceived $api_opt ;#dups ok - } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - if {$OPT_ANY} { - set newval [lindex $remaining_rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any|-arbitrary true - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $remaining_rawargs - #set values [list {*}$pre_values {*}$remaining_rawargs] ;#no -flags detected - set arglist [list] - } - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> arglist: $arglist" - #puts stderr "get_dict--> leaders: $leaders" - #puts stderr "get_dict--> values: $values" - #} - - #--------------------------------------- - set ordered_opts [dict create] - set unaliased_opts [lmap v $OPT_NAMES {lindex [split $v |] end}] - #unaliased_opts is list of 'api_opt' (handle aliases of form -a1|-a2|-api_opt e.g -fg|-foreground) - foreach o $unaliased_opts optset $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $optset]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $optset] - } - } - #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set leadername_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - - #---------------------------------------- - #Establish firm leaders ordering - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - set start_position $positionalidx - set nameidx 0 - #MAINTENANCE - (*nearly*?) same loop logic as for value - for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} { - set leadername [lindex $LEADER_NAMES $nameidx] - #incr nameidx - set ldr [lindex $leaders $ldridx] - if {$leadername ne ""} { - set typelist [tcl::dict::get $argstate $leadername -type] - if {[llength $typelist] == 1} { - set clauseval $ldr - } else { - set clauseval [list] - incr ldridx -1 - foreach t $typelist { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername', but requires [llength $leadername] values" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadername] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $leaders $ldridx] - } - } - - if {[tcl::dict::get $argstate $leadername -multiple]} { - #if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - # #current stored ldr equals defined default - don't include default in the list we build up - # tcl::dict::set leaders_dict $leadername [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend leaders_dict $leadername $clauseval - #} - if {$leadername in $leadernames_received} { - tcl::dict::lappend leaders_dict $leadername $clauseval - } else { - tcl::dict::set leaders_dict $leadername [list $clauseval] - } - set leadername_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $clauseval - set leadername_multiple "" - incr nameidx - } - lappend leadernames_received $leadername - } else { - if {$leadername_multiple ne ""} { - set typelist [tcl::dict::get $argstate $leadername_multiple -type] - if {[llength $typelist] == 1} { - set clauseval $ldr - } else { - set clauseval [list] - incr ldridx -1 - foreach t $typelist { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires [llength $leadername_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadername_multiple] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $leaders $ldridx] - } - } - tcl::dict::lappend leaders_dict $leadername_multiple $clauseval - #name already seen - but must add to leadernames_received anyway (as with opts and values) - lappend leadernames_received $leadername_multiple - } else { - if {$LEADER_UNNAMED} { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } else { - set msg "Bad number of leaders for %caller%. Received more leaders than can be assigned to argument names. (set '@leaders -unnamed true' to allow unnamed leaders)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $ldridx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_leaders_no_phantom_default - foreach leadername [dict keys $leaders_dict] { - if {[string is integer -strict $leadername]} { - #ignore leadername that is a positionalidx - #review - always trailing - could use break? - continue - } - if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset leaders_dict $leadername - } - } - #----------------------------------------------------- - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #Establish firm values ordering - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - #set ALL valnames to lock in positioning - #note - later we need to unset any optional that had no default and was not received (no phantom default) - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - #MAINTENANCE - (*nearly*?) same loop logic as for leaders - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - set val [lindex $values $validx] - if {$valname ne ""} { - set valtypelist [tcl::dict::get $argstate $valname -type] - - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] - set consumed [dict get $assign_d consumed] - set resultlist [dict get $assign_d resultlist] - if {[tcl::dict::get $argstate $valname -optional]} { - if {$consumed == 0} { - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } else { - #required named arg - if {$consumed == 0} { - if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" - set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg - } else { - incr validx -1 - set valname_multiple "" - incr nameidx - continue - } - } - } - #assert can_assign != 0, we have at least one value to assign to clause - - if {[llength $valtypelist] == 1} { - set clauseval $val - } else { - #clauseval must contain as many elements as the max length of -types! - #(empty-string/default for optional (?xxx?) clause members) - set clauseval $resultlist - #_get_dict_can_assign has only validated clause-length and literals match - #we assign and leave further validation for main validation loop. - incr validx -1 - incr validx $consumed - if {$validx > [llength $values]-1} { - error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valname] ] -argspecs $argspecs]] $msg - } - - #for {set i 0} {$i < $consumed} {incr i} { - # incr validx - # if {$validx > [llength $values]-1} { - # set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - # return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valname] ] -argspecs $argspecs]] $msg - # } - # #lappend clauseval [lindex $values $validx] - #} - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - #if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - # #current stored val equals defined default - don't include default in the list we build up - # tcl::dict::set values_dict $valname [list $clauseval] ;#important to treat first element as a list - #} else { - # tcl::dict::lappend values_dict $valname $clauseval - #} - if {$valname in $valnames_received} { - tcl::dict::lappend values_dict $valname $clauseval - } else { - tcl::dict::set values_dict $valname [list $clauseval] - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $clauseval - set valname_multiple "" - incr nameidx - } - lappend valnames_received $valname - } else { - if {$valname_multiple ne ""} { - set valtypelist [tcl::dict::get $argstate $valname_multiple -type] - if {[llength $valname_multiple] == 1} { - set clauseval $val - } else { - set clauseval [list] - incr validx -1 - for {set i 0} {$i < [llength $valtypelist]} {incr i} { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname_multiple', but requires [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valtypelist] ] -argspecs $argspecs]] $msg - } - lappend clauseval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $clauseval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - if {$VAL_UNNAMED} { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } else { - set msg "Bad number of values for %caller%. Received more values than can be assigned to argument names. (set '@values -unnamed true' to allow unnamed values)" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list toomanyarguments [llength $values] index $positionalidx] -argspecs $argspecs]] $msg - } - } - } - set positionalidx [expr {$start_position + $validx + 1}] - } - #----------------------------------------------------- - #satisfy test parse_withdef_values_no_phantom_default - foreach vname [dict keys $values_dict] { - if {[string is integer -strict $vname]} { - #ignore vname that is a positionalidx - #review - always trailing - could break? - continue - } - if {$vname ni $valnames_received && ![dict exists $VAL_DEFAULTS $vname]} { - #remove the name with empty-string default we used to establish fixed order of names - #The 'values' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. - dict unset values_dict $vname - } - } - #----------------------------------------------------- - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) - #however - if -any|-arbitrary is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] - if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { - set full_missing [dict get $lookup_optset $missing] - set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - #--------------------------------------------------------------------------------------------- - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - #--------------------------------------------------------------------------------------------- - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - if {[string match -* $argname]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - if {[dict exists $lookup_optset $argname]} { - set argname [dict get $lookup_optset $argname] - } - } - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set typelist [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$argname in $receivednames && $has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choiceprefixreservelist [Dict_getdef $thisarg -choiceprefixreservelist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #todo - don't add to validation lists if not in receivednames - if {$argname ni $receivednames} { - set vlist [list] - set vlist_check_validate [list] - } else { - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - set type [lindex $typelist 0] - if {[llength $vlist]} { - - switch -- $type { - literal { - foreach e $vlist { - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - lassign {} low high ;#set both empty - set has_range 0 - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - set has_range 1 - } - } - foreach e $vlist e_check $vlist_check { - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$has_range} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - - - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set typelist [dict get $arginfo -type] - if {[llength $typelist] == 1} { - set tp [lindex $typelist 0] - if {$tp eq "literal"} { - set clause [lindex $argname end] - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set clause $match - } else { - set clause $I$argname$RST - } - } else { - set n [expr {[llength $typelist]-1}] - set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types - set clause "" - foreach typespec $typelist elementname $name_tail { - #elementname will commonly be empty - if {[string match {\?*\?} $typespec]} { - set tp [string range $typespec 1 end-1] - set member_optional 1 - } else { - set tp $typespec - set member_optional 0 - } - if {$tp eq "literal"} { - set c $elementname - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set c $match - } else { - set c $I$tp$RST - } - if {$member_optional} { - append clause " " "(?$c?)" - } else { - append clause " " $c - } - } - set clause [string trimleft $clause] - } - - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$RST?..." - set display "?$clause?..." - } else { - set display "?$clause?" - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "?[lindex [dict get $arginfo -choices] 0]?" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display "?$argname?" - #} else { - # set display "?$I$argname$RST?" - #} - } - } else { - if {[dict get $arginfo -multiple]} { - #set display "$I$argname$RST ?$I$argname$RST?..." - set display "$clause ?$clause?..." - } else { - set display $clause - #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - # set display "[lindex [dict get $arginfo -choices] 0]" - #} elseif {[dict get $arginfo -type] eq "literal"} { - # set display $argname - #} else { - # set display "$I$argname$RST" - #} - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - #JJJ - #REVIEW - #lappend params [subst -nocommands -novariables $expression] - lappend params $expression - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.8 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.9.tm b/src/bootsupport/modules/punk/args-0.2.tm similarity index 67% rename from src/vfs/_vfscommon.vfs/modules/punk/args-0.1.9.tm rename to src/bootsupport/modules/punk/args-0.2.tm index 717c0b47..7710fa00 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.9.tm +++ b/src/bootsupport/modules/punk/args-0.2.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.1.9 +# Application punk::args 0.2 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.9] +#[manpage_begin punkshell_module_punk::args 0 0.2] #[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 --}] @@ -268,6 +268,7 @@ tcl::namespace::eval punk::args::register { #[list_end] [comment {--- end definitions namespace punk::args::register ---}] } +tcl::namespace::eval ::punk::args {} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Base namespace @@ -408,6 +409,8 @@ tcl::namespace::eval punk::args { -body (override autogenerated arg info for form) %B%@doc%N% ?opt val...? directive-options: -name -url + %B%@examples%N% ?opt val...? + directive-options: -help %B%@seealso%N% ?opt val...? directive-options: -name -url (for footer - unimplemented) @@ -438,9 +441,35 @@ tcl::namespace::eval punk::args { and trailing values also take spec-options: -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. + A typenamelist represents a multi-value clause where each + value must match the specified type in order. This is not + valid for flags - which can only take a single value. + + typename and entries in typenamelist can take 2 forms: + 1) basic form: elements of llength 1 such as a simple type, + or a pipe-delimited set of type-alternates. + e.g for a single typename: + -type int, -type int|char, -type int|literal(abc) + e.g for a typenamelist + -type {int double}, -type {int|char double} + 2) special form: elements of variable length + e.g for a single typename: + -type {{literal |}} + -type {{literal | | literal (}} + e.g for a typenamelist + -type {{literal |} {stringstartswith abc | int}} + The 2 forms can be mixed: + -type {{literal |} {stringstartswith a|c | int} literal(xyz)|int} + + Defaults to string. If no other restrictions + are required, choosing -type any does the least validation. recognised types: + any + (unvalidated - accepts anything) + none + (used for flags/switches only. Indicates this is + a 'solo' flag ie accepts no value) + Not valid as a member of a clause's typenamelist. int integer number @@ -463,11 +492,9 @@ tcl::namespace::eval punk::args { string (also any of the 'string is' types such as xdigit, graph, punct, lower etc) - any - (unvalidated - accepts anything) - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) + -type string on its own does not need validation, + but still checks for string-related restrictions + such as regexprefail, & minsize literal() (exact match for string) @@ -475,6 +502,9 @@ tcl::namespace::eval punk::args { (prefix match for string, other literal and literalprefix entries specified as alternates using | are used in the calculation) + stringstartswith() + (value must match glob *) + The value of string must not contain pipe char '|' Note that types can be combined with | to indicate an 'or' operation @@ -592,7 +622,8 @@ tcl::namespace::eval punk::args { inner loops in more performance-sensitive code. " @values -min 1 -max -1 - text -type string -multiple 1 -help\ + #text should be a well-formed Tcl list + text -type list -multiple 1 -help\ {Block(s) of text representing the argument definition for a command. At least one must be supplied. If multiple, they are joined together with \n. Using multiple text arguments may be useful to mix curly-braced and double-quoted @@ -633,7 +664,7 @@ tcl::namespace::eval punk::args { proc New_command_form {name} { #probably faster to inline a literal dict create in the proc than to use a namespace variable set leaderdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -648,7 +679,7 @@ tcl::namespace::eval punk::args { -ensembleparameter 0\ ] set optdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 1\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -667,7 +698,7 @@ tcl::namespace::eval punk::args { #parsekey is name of argument to use as a key in punk::args::parse result dicts set valdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -1031,6 +1062,7 @@ tcl::namespace::eval punk::args { #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table set seealso_info {} set keywords_info {} + set examples_info {} ###set leader_min 0 ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit #set leader_max "" @@ -1162,6 +1194,8 @@ tcl::namespace::eval punk::args { } } default { + #NOTE - this is switch arm for the literal "default" (@default) - not the default arm of the switch block! + #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) @@ -1203,7 +1237,29 @@ tcl::namespace::eval punk::args { # arity system ? #handle multiple parsing styles based on arities and keyword positions (and/or flags?) #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each + + # @form "-synopsis" is optional - and only exists in case the user really wants + # to display something different. The system should generate consistent synopses + # with appropriate italics/bracketing etc. + # For manual -synopsis - features such as italics must be manually added. + + #spitballing.. + #The punk::args parser should generally be able to determine the appropriate form based + #on supplied arguments, e.g automatically using argument counts and matching literals. + #We may need to support some hints for forcing more efficient -form discriminators + # + # e.g compare with -takewhenargsmodulo that is available on @leaders + + #the -arities idea below is a rough one; potentially something to consider.. but + #we want to be able to support command completion.. and things like literals should probably + #take preference for partially typed commands.. as flipping to other forms based on argcount + #could be confusing. Need to match partial command to closest form automatically but allow + #user to lock in a form interactively and see mismatches (?) + #Probably the arity-ranges of a form are best calculated automatically rather than explicitly, + #otherwise we have a strong potential for misdefinition.. (conflict with defined leaders,opts,values) + #The way forward might be to calculate some 'arity' structure from the forms to aid in form-discrimination at arg parse time. + #(this is currently covered in some ways by the LEADER_MIN,LEADER_MAX,OPT_MIN,OPT_MAX,VAL_MIN,VAL_MAX members of the FORMS dict.) + # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ # -arities { # 2 @@ -1231,7 +1287,6 @@ tcl::namespace::eval punk::args { # } #todo - #can we generate a form synopsis if -synopsis not supplied? #form id can be list of ints|names?, or * if {[dict exists $at_specs -form]} { @@ -1552,7 +1607,7 @@ tcl::namespace::eval punk::args { -min - -minvalues { if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is 0. got $v @id:$DEF_definition_id" } #set val_min $v dict set F $fid VAL_MIN $v @@ -1560,7 +1615,7 @@ tcl::namespace::eval punk::args { -max - -maxvalues { if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" } #set val_max $v dict set F $fid VAL_MAX $v @@ -1666,8 +1721,11 @@ tcl::namespace::eval punk::args { #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? set keywords_info [dict merge $keywords_info $at_specs] } + examples { + set examples_info [dict merge $examples_info $at_specs] + } default { - error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @examples @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" } } #record_type directive @@ -1859,98 +1917,88 @@ tcl::namespace::eval punk::args { foreach {spec specval} $argdef_values { #literal-key switch - bytecompiled to jumpTable switch -- $spec { - -form { - - } + -form {} -type { #todo - could be a list e.g {any int literal(Test)} #case must be preserved in literal bracketed part set typelist [list] foreach typespec $specval { - set lc_typespec [tcl::string::tolower $typespec] - if {[string match {\?*\?} $lc_typespec]} { - set lc_type [string range $lc_typespec 1 end-1] + if {[string match {\?*\?} $typespec]} { + set tspec [string range $typespec 1 end-1] set optional_clausemember true } else { - set lc_type $lc_typespec + set tspec $typespec set optional_clausemember false } - #normalize here so we don't have to test during actual args parsing in main function - set normtype "" ;#assert - should be overridden in all branches of switch - switch -- $lc_type { - int - integer { - set normtype int - } - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - set normtype double - } - bool - boolean { - set normtype bool - } - char - character { - set normtype char - } - dict - dictionary { - set normtype dict - } - index - indexexpression { - set normtype indexexpression - } - "" - none - solo { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $specval] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - set normtype none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + set type_alternatives [_split_type_expression $tspec] + set normlist [list] + foreach alt $type_alternatives { + set firstword [lindex $alt 0] + set lc_firstword [tcl::string::tolower $firstword] + #normalize here so we don't have to test during actual args parsing in main function + set normtype "" ;#assert - should be overridden in all branches of switch + switch -- $lc_firstword { + int - integer {set normtype int} + double - float { + #review - user may wish to preserve 'float' in help display - consider how best to implement + set normtype double + } + bool - boolean {set normtype bool} + char - character {set normtype char} + dict - dictionary {set normtype dict} + index - indexexpression {set normtype indexexpression} + "" - none - solo { + if {$is_opt} { + #review - are we allowing clauses for flags? + #e.g {-flag -type {int int}} + #this isn't very tcl like, where we'd normally mark the flag with -multiple true and + # instead require calling as: -flag -flag + #It seems this is a reasonably rare/unlikely requirement in most commandline tools. + + if {[llength $specval] > 1} { + #makes no sense to have 'none' in a clause + error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" + } + #tcl::dict::set spec_merged -type none + set normtype none + if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { + tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + } + } else { + #solo only valid for flags + error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } else { - #solo only valid for flags - error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } - any - anything { - set normtype any - } - ansi - ansistring { - set normtype ansistring - } - string - globstring { - set normtype $lc_type - } - literal { - if {$is_opt} { - error "punk::args::resolve - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" + any - anything {set normtype any} + ansi - ansistring {set normtype ansistring} + string - globstring {set normtype $lc_firstword} + literal { + #value was split out by _split_type_expression + set normtype literal([lindex $alt 1]) } - #value is the name of the argument - set normtype literal - } - default { - if {[string match literal* $lc_type]} { - #typespec may or may not be of form ?xxx? - set literal_tail [string range [string trim $typespec ?] 7 end] - set normtype literal$literal_tail - } else { + literalprefix { + set normtype literalprefix([lindex $alt 1]) + } + stringstartswith { + set normtype stringstartswith([lindex $alt 1]) + } + stringendswith { + set normtype stringendswith([lindex $alt 1]) + } + default { #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - set normtype $lc_type + #todo + set normtype $alt } } + lappend normlist $normtype } + set norms [join $normlist |] if {$optional_clausemember} { - lappend typelist ?$normtype? + lappend typelist ?$norms? } else { - lappend typelist $normtype + lappend typelist $norms } } tcl::dict::set spec_merged -type $typelist @@ -2082,7 +2130,7 @@ tcl::namespace::eval punk::args { if {$is_opt} { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize if {$argname eq "--"} { #force -type none - in case no -type was specified and @opts -type is some other default such as string tcl::dict::set spec_merged -type none @@ -2092,7 +2140,7 @@ tcl::namespace::eval punk::args { } } else { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize } tcl::dict::set F $fid ARG_INFO $argname $spec_merged #review existence of -default overriding -optional @@ -2229,6 +2277,8 @@ tcl::namespace::eval punk::args { doc_info $doc_info\ package_info $package_info\ seealso_info $seealso_info\ + keywords_info $keywords_info\ + examples_info $examples_info\ id_info $id_info\ FORMS $F\ form_names [dict keys $F]\ @@ -2259,9 +2309,9 @@ tcl::namespace::eval punk::args { namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} + variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @examples @formdisplay @seealso @leaders @opts @values leaders opts values} variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} + directives {@id @package @cmd @ref @doc @examples @formdisplay @seealso} argumenttypes {leaders opts values} remaining_defaults {@leaders @opts @values} } @@ -2468,7 +2518,7 @@ tcl::namespace::eval punk::args { dict set resultdict @id [list -id [dict get $specdict id]] } } - foreach directive {@package @cmd @doc @seealso} { + foreach directive {@package @cmd @doc @examples @seealso} { set dshort [string range $directive 1 end] if {"$directive" in $included_directives} { if {[dict exists $opt_override $directive]} { @@ -2482,6 +2532,7 @@ tcl::namespace::eval punk::args { } #todo @formdisplay + #todo @ref ? #output ordered by leader, option, value @@ -2533,7 +2584,7 @@ tcl::namespace::eval punk::args { } } } - @package - @cmd - @doc - @seealso { + @package - @cmd - @doc - @examples - @seealso { if {"$type" in $included_directives} { set tp [string range $type 1 end] ;# @package -> package if {[dict exists $opt_override $type]} { @@ -2715,6 +2766,10 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + proc aliases {} { + variable aliases + punk::lib::showdict $aliases + } proc set_alias {alias id} { variable aliases dict set aliases $alias $id @@ -3077,62 +3132,81 @@ tcl::namespace::eval punk::args { "Ordinal index or name of command form" }] ] - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } + variable arg_error_CLR array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] variable arg_error_CLR_nocolour array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" variable arg_error_CLR_info array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] variable arg_error_CLR_error array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] + + proc _argerror_load_colours {{forcereload 0}} { + variable arg_error_CLR + #todo - option for reload/retry? + if {!$forcereload && [array size arg_error_CLR] > 0} { + return + } + + if {[catch {package require punk::ansi} errMsg]} { + puts stderr "punk::args FAILED to load punk::ansi\n$errMsg" + proc ::punk::args::a {args} {} + proc ::punk::args::a+ {args} {} + } else { + namespace eval ::punk::args { + namespace import ::punk::ansi::a ::punk::ansi::a+ + } + } + #array set arg_error_CLR {} + set arg_error_CLR(testsinglecolour) [a+ yellow] ;#A single SGR colour to test current colour on|off state (empty string vs some result - used to determine if forcereload required) + set arg_error_CLR(errormsg) [a+ brightred] + set arg_error_CLR(title) "" + set arg_error_CLR(check) [a+ brightgreen] + set arg_error_CLR(solo) [a+ brightcyan] + set arg_error_CLR(choiceprefix) [a+ underline] + set arg_error_CLR(badarg) [a+ brightred] + set arg_error_CLR(goodarg) [a+ green strike] + set arg_error_CLR(goodchoice) [a+ reverse] + set arg_error_CLR(linebase_header) [a+ white] + set arg_error_CLR(cmdname) [a+ brightwhite] + set arg_error_CLR(groupname) [a+ bold] + set arg_error_CLR(ansiborder) [a+ bold] + set arg_error_CLR(ansibase_header) [a+ bold] + set arg_error_CLR(ansibase_body) [a+ white] + variable arg_error_CLR_nocolour + #array set arg_error_CLR_nocolour {} + set arg_error_CLR_nocolour(errormsg) [a+ bold] + set arg_error_CLR_nocolour(title) [a+ bold] + set arg_error_CLR_nocolour(check) "" + set arg_error_CLR_nocolour(solo) "" + set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment + set arg_error_CLR_nocolour(goodarg) [a+ strike] + set arg_error_CLR_nocolour(cmdname) [a+ bold] + set arg_error_CLR_nocolour(linebase_header) "" + set arg_error_CLR_nocolour(linebase) "" + set arg_error_CLR_nocolour(ansibase_body) "" + variable arg_error_CLR_info + #array set arg_error_CLR_info {} + set arg_error_CLR_info(errormsg) [a+ brightred bold] + set arg_error_CLR_info(title) [a+ brightyellow bold] + set arg_error_CLR_info(check) [a+ brightgreen bold] + set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_info(groupname) [a+ cyan bold] + set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] + set arg_error_CLR_info(ansibase_header) [a+ cyan] + set arg_error_CLR_info(ansibase_body) [a+ white] + variable arg_error_CLR_error + #array set arg_error_CLR_error {} + set arg_error_CLR_error(errormsg) [a+ brightred bold] + set arg_error_CLR_error(title) [a+ brightcyan bold] + set arg_error_CLR_error(check) [a+ brightgreen bold] + set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_error(groupname) [a+ cyan bold] + set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] + set arg_error_CLR_error(ansibase_header) [a+ yellow] + set arg_error_CLR_error(ansibase_body) [a+ white] + } #bas ic recursion blocker @@ -3174,7 +3248,21 @@ tcl::namespace::eval punk::args { error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" } + #set arg_error_CLR(testsinglecolour) [a+ brightred] + upvar ::punk::args::arg_error_CLR 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)]} { + set terminal_colour_is_on [expr {[string length [a+ yellow]]}] + set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + if {$terminal_colour_is_on ^ $error_colour_is_on} { + #results differ + set forcereload 1 + } + } + _argerror_load_colours $forcereload + if {[llength $args] %2 != 0} { + set arg_error_isrunning 0 error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" } @@ -3183,7 +3271,12 @@ 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] + #----------------------- + #todo!! make changeable from config file + #JJJ 2025-07-16 set returntype table ;#table as string + #set returntype string + #---------------------- set as_error 1 ;#usual case is to raise an error set scheme error set form 0 @@ -3267,12 +3360,11 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR switch -- $scheme { nocolour { variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour + array set CLR [array get arg_error_CLR_nocolour] } info { variable arg_error_CLR_info @@ -3554,8 +3646,7 @@ tcl::namespace::eval punk::args { set tail "" } else { set idlen [string length $id] - set prefix [string range $opt 0 $idlen-1] - set tail [string range $opt $idlen end] + lassign [punk::lib::string_splitbefore $opt $idlen] prefix tail } lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail } @@ -3623,30 +3714,37 @@ tcl::namespace::eval punk::args { } else { set thisgroup_parsekey "" } - if {$thisgroup eq ""} { + #footer/line? if {$use_table} { - $t add_row [list " --- " "" "" "" ""] + $t add_row [list " " "" "" "" ""] } else { - lappend errlines " --- " + lappend errlines " " } + + if {$thisgroup eq ""} { } else { #SHOW group 'header' (not really a table header - just another row) set help "" if {[dict exists $form_dict OPT_GROUPS $thisgroup -help]} { set help [dict get $form_dict OPT_GROUPS $thisgroup -help] } + if {$thisgroup_parsekey eq ""} { + set groupinfo "(documentation group)" + } else { + set groupinfo "(common flag group)\nkey:$thisgroup_parsekey" + } if {$use_table} { - $t add_row [list " $thisgroup" "(flag group)" "" "" $help] + $t add_row [list " $thisgroup" $groupinfo "" "" $help] if {$arg eq $badarg} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { + } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { #review - formatting will be all over the shop due to newlines in typesshow, help - set linetail " TYPE:(flag group)" - set arghelp "[a+ bold] $thisgroup$RST $linetail" + #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" + set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] append arghelp \n if {$arg eq $badarg} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] @@ -3662,6 +3760,12 @@ tcl::namespace::eval punk::args { set lastgroup $thisgroup set lastgroup_parsekey $thisgroup_parsekey } + if {[dict exists $arginfo -parsekey]} { + set mypkey [dict get $arginfo -parsekey] + if {$mypkey eq "$lastgroup_parsekey" || $mypkey eq [string trimright [lindex [split $arg |] end] =]} { + set hint "" + } + } } if {[dict exists $arginfo -default]} { @@ -3799,14 +3903,15 @@ tcl::namespace::eval punk::args { } else { set shortestid [dict get $idents $c] } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } + lassign [punk::lib::string_splitbefore $c [string length $shortestid]] prefix tail + #if {$shortestid eq $c} { + # set prefix $c + # set tail "" + #} else { + # set idlen [string length $shortestid] + # set prefix [string range $c 0 $idlen-1] + # set tail [string range $c $idlen end] + #} set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] if {[llength $markers]} { set mk " [join $markers {}]" @@ -4566,37 +4671,55 @@ tcl::namespace::eval punk::args { #set v [lindex $values end-$ridx] set v [lindex $all_remaining end] set tp [lindex $typelist 0] + # ----------------- + set tp [string trim $tp ?] ;#shouldn't be necessary #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole name should be marked -optional true instead. - set tp [string trim $tp ?] - foreach tp_member [split $tp |] { - switch -glob $tp { - literal* { + #we shouldn't have an optional clause member if there is only one member - the whole argument should be marked -optional true instead. + # ----------------- + + #todo - support complex type members such as -type {{literal a|b} int OR} + #for now - require llength 1 - simple type such as -type {literal(ab)|int} + if {[llength $tp] !=1} { + error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + } + + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + switch -exact -- [lindex $tp_alternative 0] { + literal { set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #plain "literal" without bracketed specifier - match to argument name - set match $clausename - } + set match [lindex $tp_alternative 1] if {$v eq $match} { set alloc_ok 1 - lpop all_remaining + ledit all_remaining end end if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + ledit tailnames end end } #the type (or one of the possible type alternates) matched a literal break } } - "stringprefix(*" { - set pfx [string range $tp 13 end-1] + stringstartswith { + set pfx [lindex $tp_alternative 1] if {[string match "$pfx*" $v} { set alloc_ok 1 set alloc_ok 1 - lpop all_remaining + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + break + } + + } + stringendswith { + set sfx [lindex $tp_alternative 1] + if {[string match "*$sfx" $v} { + set alloc_ok 1 + set alloc_ok 1 + ledit all_remaining end end if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + ledit tailnames end end } break } @@ -4612,6 +4735,9 @@ tcl::namespace::eval punk::args { } } else { + #todo - use _split_type_expression + + #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) #This is better caught during definition. #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} @@ -4621,14 +4747,11 @@ tcl::namespace::eval punk::args { set alloc_count 0 #clause name may have more entries than types - extras at beginning are ignored set rtypelist [lreverse $typelist] - set rclausename [lrange [lreverse $clausename] 0 [llength $typelist]-1] - #assert length of rtypelist >= $rclausename set alloc_ok 0 set reverse_type_index 0 #todo handle type-alternates # for example: -type {string literal(x)|literal(y)} - foreach tp $rtypelist membername $rclausename { - #(membername may be empty if not enough elements) + foreach tp $rtypelist { #set rv [lindex $rcvals end-$alloc_count] set rv [lindex $all_remaining end-$alloc_count] if {[string match {\?*\?} $tp]} { @@ -4640,13 +4763,7 @@ tcl::namespace::eval punk::args { switch -glob $tp { literal* { set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #if membername empty - equivalent to "literal()" - matches empty string literal - #edgecase - possibly? no need for empty-string literals - but allow it without error. - set match $membername - } + set match [string range $litinfo 1 end-1] #todo -literalprefix if {$rv eq $match} { set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok @@ -4660,15 +4777,13 @@ tcl::namespace::eval punk::args { } } } - "stringprefix(*" { - set pfx [string range $tp 13 end-1] + "stringstartswith(*" { + set pfx [string range $tp 17 end-1] if {[string match "$pfx*" $tp]} { set alloc_ok 1 incr alloc_count } else { - if {$clause_member_optional} { - # - } else { + if {!$clause_member_optional} { set alloc_ok 0 break } @@ -4679,6 +4794,7 @@ tcl::namespace::eval punk::args { #review - optional non-literal makes things harder.. #we don't want to do full type checking here - but we now risk allocating an item that should actually #be allocated to the previous value + # todo - lsearch to next literal or non-optional? set prev_type [lindex $rtypelist $reverse_type_index+1] if {[string match literal* $prev_type]} { set litinfo [string range $prev_type 7 end] @@ -4686,7 +4802,6 @@ tcl::namespace::eval punk::args { if {[string match (*) $litinfo]} { set match [string range $litinfo 1 end-1] } else { - #prev membername set match [lindex $rclausename $reverse_type_index+1] } if {$rv ne $match} { @@ -4712,7 +4827,8 @@ tcl::namespace::eval punk::args { set all_remaining [lrange $all_remaining 0 end-$alloc_count] #don't lpop if -multiple true if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + #lpop tailnames + ledit tailnames end end } } else { break @@ -4740,11 +4856,11 @@ tcl::namespace::eval punk::args { set alloc_count 0 set resultlist [list] set n [expr {[llength $thistype]-1}] - #name can have more or less items than typelist - set thisnametail [lrange $thisname end-$n end] set tpidx 0 set newtypelist $thistype - foreach tp $thistype membername $thisnametail { + set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] + foreach tp $thistype { + #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) set v [lindex $all_remaining $alloc_count] if {[string match {\?*\?} $tp]} { set clause_member_optional 1 @@ -4754,137 +4870,130 @@ tcl::namespace::eval punk::args { set tp [string trim $tp ?] set member_satisfied 0 + if {$has_choices} { + #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 + set member_satisfied 1 + } - #----------------------------------------------------------------------------------- - #first build category lists of any literal,literalprefix,stringprefix,other - # - set ctg_literals [list] - set ctg_literalprefixes [list] - set ctg_stringprefixes [list] - set ctg_other [list] - set dict_member_match [dict create] - foreach tp_member [split $tp |] { - #JJJJ - switch -glob -- $tp_member { - literal* { - if {[string match literalprefix* $tp_member]} { - set litinfo [string range $tp_member 13 end] - if {[string match (*) $litinfo]} { - lappend ctg_literalprefixes [string range $litinfo 1 end-1] - } else { - lappend ctg_literalprefixes $membername - } - dict set dict_member_match $tp_member [lindex $ctg_literalprefixes end] - } else { - set litinfo [string range $tp_member 7 end] - if {[string match (*) $litinfo]} { - lappend ctg_literals [string range $litinfo 1 end-1] - } else { - lappend ctg_literals $membername - } - dict set dict_member_match $tp_member [lindex $ctg_literals end] + + if {!$member_satisfied} { + #----------------------------------------------------------------------------------- + #first build category lists of any literal,literalprefix,stringstartwith,other + # + set ctg_literals [list] + set ctg_literalprefixes [list] + set ctg_stringstartswith [list] + set ctg_stringendswith [list] + set ctg_other [list] + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + #JJJJ + lassign $tp_alternative t textra + switch -exact -- $t { + literal { + lappend ctg_literals $textra + } + literalprefix { + lappend ctg_literalprefixes $textra + } + stringstartswith { + lappend ctg_stringstartswith $textra + } + stringendswith { + lappend ctg_stringendswith $textra + } + default { + lappend ctg_other $tp_alternative } - } - "stringprefix(*" { - set pfx [string range $tp_member 13 end-1] - lappend ctg_stringprefixes $pfx - } - default { - lappend ctg_other $tp_member } } - } - #----------------------------------------------------------------------------------- - #asert - each tp_member is a key in dict_member_match - if {[llength $ctg_other] > 0} { - #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align - #we don't do full validation here -leave main validation for later (review) - foreach tp_member $ctg_other { - switch -exact -- $tp_member { - int { - if {[string is integer -strict $v]} { - set member_satisfied 1 - break + #----------------------------------------------------------------------------------- + if {[llength $ctg_other] > 0} { + #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align + #we don't do full validation here -leave main validation for later (review) + foreach tp_alternative $ctg_other { + switch -exact -- $tp_alternative { + int { + if {[string is integer -strict $v]} { + set member_satisfied 1 + break + } } - } - double { - if {[string is double -strict $v]} { - set member_satisfied 1 - break + double { + if {[string is double -strict $v]} { + set member_satisfied 1 + break + } } - } - bool { - if {[string is boolean -strict $v]} { - set member_satisfied 1 - break + bool { + if {[string is boolean -strict $v]} { + set member_satisfied 1 + break + } } - } - number { - if {[string is integer -strict $v] || [string is double -strict $v]} { - set member_satisfied 1 - break + number { + if {[string is integer -strict $v] || [string is double -strict $v]} { + set member_satisfied 1 + break + } } - } - dict { - if {[string is dict $v]} { + dict { + if {[punk::args::lib::string_is_dict $v]} { + set member_satisfied 1 + break + } + } + default { + #REVIEW!!! + #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. + #todo - catch/detect in caller set member_satisfied 1 break } } - default { - #REVIEW!!! - #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. - #todo - catch/detect in caller - set member_satisfied 1 - break - } } } } + if {!$member_satisfied && ([llength $ctg_literals] || [llength $ctg_literalprefixes])} { if {$v in $ctg_literals} { set member_satisfied 1 + lset newtypelist $tpidx validated-$tp } else { #ctg_literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed #(exact match would have been caught in other branch of this if) - #review - how does ctg_stringprefixes affect prefix calc for literals? + #review - how does ctg_stringstartswith affect prefix calc for literals? set full_v [tcl::prefix::match -error "" [list {*}$ctg_literals {*}$ctg_literalprefixes] $v] if {$full_v ne "" && $full_v ni $ctg_literals} { #matched prefix must be for one of the entries in ctg_literalprefixes - valid set member_satisfied 1 + set v $full_v ;#map prefix given as arg to the full literalprefix value + lset newtypelist $tpidx validated-$tp } } } - if {!$member_satisfied && [llength $ctg_stringprefixes]} { - foreach pfx $ctg_stringprefixes { + if {!$member_satisfied && [llength $ctg_stringstartswith]} { + foreach pfx $ctg_stringstartswith { if {[string match "$pfx*" $v]} { set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + #review. consider multi-word typespec with RPN? + # {*}$tp_alternative validated + break + } + } + } + if {!$member_satisfied && [llength $ctg_stringendswith]} { + foreach pfx $ctg_stringendswith { + if {[string match "*$pfx" $v]} { + set member_satisfied 1 + lset newtypelist $tpidx validated-$tp break } } } - #foreach tp_member [split $tp |] { - # if {[string match literal* $tp_member]} { - # #todo - support literal prefix-matching - # #e.g see ::readFile filename ?text|binary? - must accept something like readfile xxx.txt b - # set litinfo [string range $tp_member 7 end] - # if {[string match (*) $litinfo]} { - # set match [string range $litinfo 1 end-1] - # } else { - # set match $membername - # } - # set match [dict get $dict_member_match $tp_member] - # if {$v eq $match} { - # set member_satisfied 1 - # break - # } - # } else { - # #we don't validate here -leave validation for later (review) - # set member_satisfied 1 - # break - # } - #} + if {$member_satisfied} { if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { @@ -4897,6 +5006,7 @@ tcl::namespace::eval punk::args { lappend resultlist "" } } else { + #may have satisfied one of the basic type tests above lappend resultlist $v incr alloc_count } @@ -4929,121 +5039,1821 @@ tcl::namespace::eval punk::args { #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed 0 resultlist {} typelist $thistype] } #puts ">>>> _get_dict_can_assign_value $d" return $d } - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" + #_split_type_expression + #only handles toplevel 'or' for type_expression e.g int|char + #we have no mechanism for & - (although it would be useful) + #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) + #or perhaps more performant, RPN to avoid bracket parsing + #if literal(..), literalprefix(..), stringstartswith(..) etc can have pipe symbols and brackets etc - we can't just use split + #if we require -type to always be treated as a list - and if an element is length 1 - require it to + #have properly balanced brackets that don't contain | ( ) etc we can simplify - REVIEW + + #consider: + #1 basic syntax - only OR supported - limits on what chars can be put in 'textn' elements. + #mode -type literalprefix(text1)|literalprefix(text2) -optional 1 + #2 expanded syntax - supports arbitrary chars in 'textn' elements - but still doesn't support more complex OR/AND logic + #mode -type {{literalprefix text1 | literalprefix text2}} + #3 RPN (reverse polish notation) - somewhat unintuitive, but allows arbitrary textn, and complex OR/AND logic without brackets. + #(forth like - stack based definition of types) + #mode -type {literalprefix text1 literalprefix text2 OR} + #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + + proc _split_type_expression {type_expression} { + if {[llength $type_expression] == 1} { + #simple expressions of length one must be splittable on | + #disallowed: things such as literal(|) or literal(x|etc)|int + #these would have to be expressed as {literal |} and {literal x|etc | int} + set or_type_parts [split $type_expression |] + set type_alternatives [list] + foreach t $or_type_parts { + if {[regexp {([^\(^\)]*)\((.*)\)$} $t _ name val]} { + lappend type_alternatives [list $name $val] + } else { + lappend type_alternatives $t } } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] + return $type_alternatives } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" + error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + #todo + #RPN reverse polish notation + #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + #equivalent logic: ((stringstartswith(x)|stringstartswith(y))&stringendswith(z))|int + # {int ; stringstartswith x stringstartswith y OR } + + #experimental.. seems like a pointless syntax. + #may as well just use list of lists with |(or) as the intrinsic operator instead of parsing this + #e.g {stringstartswith x | literal | | int} + set type_alternatives [list] + set expect_separator 0 + for {set w 0} {$w < [llength $type_expression]} {incr w} { + set word [lindex $type_expression $w] + if {$expect_separator} { + if {$word eq "|"} { + #pipe could be last entry - not strictly correct, but can ignore + set expect_separator 0 + continue + } else { + error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + } + } + switch -exact -- $word { + literal - literalprefix - stringstartswith - stringendswith - stringcontains { + if {$w+1 > [llength $type_expression]} { + #premature end - no arg available for type which requires one + error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + } + lappend type_alternatives [list $word [lindex $type_expression $w+1]] + incr w ;#consume arg + set expect_separator 1 + } + default { + #simple types such as int,double,string + lappend type_alternatives $word + set expect_separator 1 + } + } } - set selected_forms [list $opt_form] + return $type_alternatives } + } + #old version + ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ### #set type $type_expression ;#todo - 'split' on | + ### set vlist $clausevalues_raw + ### set vlist_check $clausevalues_check + + ### set type_alternatives [_split_type_expression $type_expression] + ### #each type_alternative is a list of varying length depending on arguments supported by first word. + ### #TODO? + ### #single element types: int double string etc + ### #two element types literal literalprefix stringstartswith stringendswith + ### #TODO + ### set stype [lindex $type_alternatives 0] + ### #e.g int + ### #e.g {literal blah)etc} + ### set type [lindex $stype 0] + ### #switch on first word of each stype + ### # + + ### #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + ### switch -- $type { + ### any {} + ### literal { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {$e ne $testval} { + ### set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### stringstartwith { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {![string match $testval* $e]} { + ### set msg "$argclass '$argname' for %caller% requires stringstartswith value '$argname'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### list { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is list -strict $e_check]} { + ### set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### tcl::dict::for {checkopt checkval} $thisarg_checks { + ### switch -- $checkopt { + ### -minsize { + ### # -1 for disable is as good as zero + ### if {[llength $e_check] < $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### -maxsize { + ### if {$checkval ne "-1"} { + ### if {[llength $e_check] > $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### indexexpression { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[catch {lindex {} $e_check}]} { + ### set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### string - ansistring - globstring { + ### #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + ### #we possibly don't want to always have to regex on things that don't pass the other more basic checks + ### # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + ### # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + ### # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + ### # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + ### # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + ### # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + ### #todo? - way to validate both unstripped and stripped? + ### set pass_quick_list_e [list] + ### set pass_quick_list_e_check [list] + ### set remaining_e $vlist + ### set remaining_e_check $vlist_check + ### #review - order of -regexprepass and -regexprefail in original rawargs significant? + ### #for now -regexprepass always takes precedence + ### set regexprepass [tcl::dict::get $thisarg -regexprepass] + ### set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + ### if {$regexprepass ne ""} { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + ### lappend pass_quick_list_e $clauseval + ### lappend pass_quick_list_e_check $clauseval_check + ### } + ### } + ### set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] + ### set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] + ### } + ### if {$regexprefail ne ""} { + ### foreach clauseval $remaining_e clauseval_check $remaining_e_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #puts "----> checking $e vs regex $regexprefail" + ### if {[regexp $regexprefail $e]} { + ### if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + ### #review - %caller% ?? + ### set msg [tcl::dict::get $thisarg -regexprefailmsg] + ### } else { + ### set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + ### } + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### switch -- $type { + ### ansistring { + ### #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + ### #.. so we need to look at the original values in $vlist not $vlist_check + + ### #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + ### #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + ### package require punk::ansi + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![punk::ansi::ta::detect $e]} { + ### set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### globstring { + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![regexp {[*?\[\]]} $e]} { + ### set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + + ### if {[tcl::dict::size $thisarg_checks]} { + ### foreach clauseval $remaining_e_check { + ### set e_check [lindex $clauseval $clausecolumn] + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsize [dict get $thisarg_checks -minsize] + ### # -1 for disable is as good as zero + ### if {[tcl::string::length $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsize [dict get $thisarg_checks -maxsize] + ### if {$checkval ne "-1"} { + ### if {[tcl::string::length $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### number { + ### #review - consider effects of Nan and Inf + ### #NaN can be considered as 'technically' a number (or at least a special numeric value) + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign {} low high ;#set both empty + ### lassign $range low high + + ### if {"$low$high" ne ""} { + ### if {[::tcl::mathfunc::isnan $e]} { + ### set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$low eq ""} { + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### int { + ### #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is integer -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign $range low high + ### if {"$low$high" ne ""} { + ### if {$low eq ""} { + ### #lowside unspecified - check only high + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### #highside unspecified - check only low + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### #high and low specified + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### double { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is double -strict $e_check]} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -typeranges]} { + ### set ranges [dict get $thisarg_checks -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### #todo - small-value double comparisons with error-margin? review + ### #todo - empty string for low or high + ### lassign $range low high + ### if {$e_check < $low || $e_check > $high} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### bool { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is boolean -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### dict { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[llength $e_check] %2 != 0} { + ### set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsizes [dict get $thisarg_checks -minsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set minsize [lindex $minsizes $clausecolumn] + ### # -1 for disable is as good as zero + ### if {[tcl::dict::size $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsizes [dict get $thisarg_checks -maxsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set maxsize [lindex $maxsizes $clausecolumn] + ### if {$maxsize ne "-1"} { + ### if {[tcl::dict::size $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### alnum - + ### alpha - + ### ascii - + ### control - + ### digit - + ### graph - + ### lower - + ### print - + ### punct - + ### space - + ### upper - + ### wordchar - + ### xdigit { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is $type -strict $e_check]} { + ### set e [lindex $clauseval $t] + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### file - + ### directory - + ### existingfile - + ### existingdirectory { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #//review - we may need '?' char on windows + ### if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + ### #what about special file names e.g on windows NUL ? + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$type eq "existingfile"} { + ### if {![file exists $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } elseif {$type eq "existingdirectory"} { + ### if {![file isdirectory $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### char { + ### #review - char vs unicode codepoint vs grapheme? + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[tcl::string::length $e_check] != 1} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### default { + ### } + ### } + + ###} + + #new version + #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname + #common basic case: list of single item being a single value clause. + #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} + proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + #default_type_expression is for the chosen clausecolumn + #if {$argname eq "frametype"} { + #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" + #puts "--->list_of_clauses_raw : $list_of_clauses_raw" + #puts "--->list_of_clauses_check: $list_of_clauses_check" + #puts "--->$argname -type: [dict get $thisarg -type]" + #} - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - + set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn + + set default_type_alternatives [_split_type_expression $default_type_expression] + #--------------------- + #pre-calc prefix sets based on the default. + set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] + set literals [lmap v $alt_literals {lindex $v 1}] + set alt_literalprefixes [lsearch -all -inline -index 0 $default_type_alternatives literalprefix] + set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + #--------------------- + + #each type_alternative is a list of varying length depending on arguments supported by first word. + #TODO? + #single element types: int double string etc + #two element types literal literalprefix stringstartswith stringendswith + #TODO - #todo: -minmultiple -maxmultiple ? + #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $default_type_alternatives] _]] + #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + #-types {int|char|literal(ok) char double} + #we are checking a and 1 against the defaulttype_expression e.g int|char|literal(ok) (type_alternatives = {int char literal(ok)} + #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + #review: for a particular clause the active type_expression might be overridden with 'any' if the column has already passed a -choices test + # - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? + set e_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_raw *] + set check_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] + set typelist_vals_raw [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_types *] + set typelist_vals [lmap v $typelist_vals_raw {string trim $v ?}] + + set c_idx -1 + foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { + incr c_idx + set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set firstany [lsearch -exact $col_type_alternatives any] + if {$firstany > -1} { + lset clause_results $c_idx $firstany 1 + continue + } + set a_idx -1 + foreach typealt $col_type_alternatives { + incr a_idx + lassign $typealt type testval ;#testval will be empty for basic types, but applies to literal, literalprefix, stringstartswith etc. + switch -exact -- $type { + literal { + if {$e ne $testval} { + set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } + } + literalprefix { + #this specific literalprefix testval value not relevant - we're testing against all in the set of typealternates + set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + if {$match ne "" && $match ni $literals} { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } else { + set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringstartswith { + if {[string match $testval* $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringendswith { + if {[string match *$testval $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + list { + if {![tcl::string::is list -strict $e_check]} { + set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + continue + } else { + if {[dict exists $thisarg_checks -minsize]} { + # -1 for disable is as good as zero + set minsize [dict get $thisarg_checks -minsize] + if {[llength $e_check] < $minsize} { + set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exist $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$maxsize ne "-1"} { + if {[llength $e_check] > $maxsize} { + set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + indexexpression { + if {[catch {lindex {} $e_check}]} { + set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + string - ansistring - globstring { + #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + #todo? - way to validate both unstripped and stripped? + #review - order of -regexprepass and -regexprefail in original rawargs significant? + #for now -regexprepass always takes precedence + #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + set regexprepass [tcl::dict::get $thisarg -regexprepass] + set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + if {$regexprepass ne ""} { + if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + lset clause_results $c_idx $a_idx 1 + break + } + } + if {$regexprefail ne ""} { + #puts "----> checking $e vs regex $regexprefail" + if {[regexp $regexprefail $e]} { + if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + #review - %caller% ?? + set msg [tcl::dict::get $thisarg -regexprefailmsg] + } else { + set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + } + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + #review - tests? + continue + } + } + switch -- $type { + ansistring { + #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + package require punk::ansi + if {![punk::ansi::ta::detect $e]} { + set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + globstring { + if {![regexp {[*?\[\]]} $e]} { + set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsize [dict get $thisarg_checks -minsize] + # -1 for disable is as good as zero + if {[tcl::string::length $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$checkval ne "-1"} { + if {[tcl::string::length $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + number { + #review - consider effects of Nan and Inf + #NaN can be considered as 'technically' a number (or at least a special numeric value) + if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign {} low high ;#set both empty + lassign $range low high + if {"$low$high" ne ""} { + if {[::tcl::mathfunc::isnan $e]} { + set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {$low eq ""} { + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + int { + #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + if {![tcl::string::is integer -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign $range low high + if {"$low$high" ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + #high and low specified + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + double { + if {![tcl::string::is double -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg_checks -typeranges]} { + set ranges [dict get $thisarg_checks -typeranges] + set range [lindex $ranges $clausecolumn] + #todo - small-value double comparisons with error-margin? review + lassign $range low high + if {$low$high ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass $argname for %caller% must be double less than or equal to $high. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass $argname for %caller% must be double greater than or equal to $low. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$e_check < $low || $e_check > $high} { + set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + bool { + if {![tcl::string::is boolean -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + dict { + if {[llength $e_check] %2 != 0} { + set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsizes [dict get $thisarg_checks -minsize] + set minsize [lindex $minsizes $clausecolumn] + # -1 for disable is as good as zero + if {[tcl::dict::size $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type minsize $minsize] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [lindex $maxsizes $clausecolumn] + if {$maxsize ne "-1"} { + if {[tcl::dict::size $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type maxsize $maxsize] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + } + alnum - + alpha - + ascii - + control - + digit - + graph - + lower - + print - + punct - + space - + upper - + wordchar - + xdigit { + if {![tcl::string::is $type -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + file - + directory - + existingfile - + existingdirectory { + #//review - we may need '?' char on windows + if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + #what about special file names e.g on windows NUL ? + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {$type eq "existingfile"} { + if {![file exists $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } elseif {$type eq "existingdirectory"} { + if {![file isdirectory $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + lset clause_results $c_idx $a_idx 1 + } + char { + #review - char vs unicode codepoint vs grapheme? + if {[tcl::string::length $e_check] != 1} { + set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + tk_screen_units { + switch -exact -- [string index $e_check end] { + c - i - m - p { + set numpart [string range $e_check 0 end-1] + if {![tcl::string::is double $numpart]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + default { + if {![tcl::string::is double $e_check]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + default { + #default pass for unrecognised types - review. + lset clause_results $c_idx $a_idx 1 + break + } + } + } + } + + foreach clauseresult $clause_results { + if {[lsearch $clauseresult 1] == -1} { + #no pass for this clause - fetch first? error and raise + #todo - return error containing clause_indices so we can report more than one failing element at once? + foreach e $clauseresult { + switch -exact [lindex $e 0] { + errorcode { + #errorcode msg checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against type_expression $type_expression" + # puts "--->list_of_clauses_raw : $list_of_clauses_raw" + # puts "--->list_of_clauses_check: $list_of_clauses_check" + # puts "--->$argname -type: [dict get $thisarg -type]" + # } + + # set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just passed type_expression + + # #set vlist [list] + # set clauses_dict [dict create] ;#key is ordinal position, remove entries as they are satsified + # set cidx -1 + # foreach cv $list_of_clauses_raw { + # incr cidx + # #REVIEW + # #if {$clause_size ==1} { + # # lappend vlist [list $cidx [list $cv]] + # #} else { + # #lappend vlist [list $cidx $cv] ;#store the index so we can reduce vlist as we go + # dict set clauses_dict $cidx $cv + # #} + # } + # #set vlist_check [list] + # set clauses_dict_check [dict create] + # set cidx -1 + # foreach cv $list_of_clauses_check { + # incr cidx + # #if {$clause_size == 1} { + # # lappend vlist_check [list $cidx [list $cv]] + # #} else { + # #lappend vlist_check [list $cidx $cv] + # dict set clauses_dict_check $cidx $cv + # #} + # } + + # set type_alternatives [_split_type_expression $type_expression] + # #each type_alternative is a list of varying length depending on arguments supported by first word. + # #TODO? + # #single element types: int double string etc + # #two element types literal literalprefix stringstartswith stringendswith + # #TODO + + # #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + # set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $type_alternatives] _]] + # #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + # #-types {int|char|literal(ok) char double} + # #we are checking a and 1 against the type_expression int|char|literal(ok) (type_alternatives = {int char literal(ok)} + # #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + # # + + + # set a_idx -1 + # foreach typealt $type_alternatives { + # incr a_idx + + # set type [lindex $typealt 0] + # #e.g int + # #e.g {literal blah} + # #e.g {literalprefix abc} + + # #switch on first word of each typealt + # # + + # #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + # switch -- $type { + # any {} + # literal { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {$e ne $testval} { + # set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # literalprefix { + # set alt_literals [lsearch -all -inline -index 0 $type_alternatives literal] + # set literals [lmap v $alt_literals {lindex $v 1}] + # set alt_literalprefixes [lsearch -all -inline -index 0 $type_alternatives literalprefix] + # set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # #this specific literalprefix value not relevant - we're testing against all in the set of typealternates + # #set testval [lindex $typealt 1] + # set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + # if {$match ne "" && $match ni $literals} { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringstartswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match $testval* $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringendswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match *$testval $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # list { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # if {![tcl::string::is list -strict $e_check]} { + # set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } else { + # if {[dict exists $thisarg_checks -minsize]} { + # # -1 for disable is as good as zero + # set minsize [dict get $thisarg_checks -minsize] + # if {[llength $e_check] < $minsize} { + # set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exist $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$maxsize ne "-1"} { + # if {[llength $e_check] > $maxsize} { + # set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # indexexpression { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[catch {lindex {} $e_check}]} { + # set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # string - ansistring - globstring { + # #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + # #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + # #todo? - way to validate both unstripped and stripped? + # #review - order of -regexprepass and -regexprefail in original rawargs significant? + # #for now -regexprepass always takes precedence + # #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + # set regexprepass [tcl::dict::get $thisarg -regexprepass] + # set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + # if {$regexprepass ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # if {$regexprefail ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # #puts "----> checking $e vs regex $regexprefail" + # if {[regexp $regexprefail $e]} { + # if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + # #review - %caller% ?? + # set msg [tcl::dict::get $thisarg -regexprefailmsg] + # } else { + # set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + # } + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + # #review - tests? + # } + # } + # } + # switch -- $type { + # ansistring { + # #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + # #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + # #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + # #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + # package require punk::ansi + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![punk::ansi::ta::detect $e]} { + # set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # globstring { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![regexp {[*?\[\]]} $e]} { + # set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # } + + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # if {[tcl::dict::size $thisarg_checks]} { + # set passed_checks 1 + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[dict exists $thisarg_checks -minsize]} { + # set minsize [dict get $thisarg_checks -minsize] + # # -1 for disable is as good as zero + # if {[tcl::string::length $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$checkval ne "-1"} { + # if {[tcl::string::length $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } else { + # if {[lindex $clause_results $c_idx $a_idx] eq "_"} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # number { + # #review - consider effects of Nan and Inf + # #NaN can be considered as 'technically' a number (or at least a special numeric value) + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign {} low high ;#set both empty + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {[::tcl::mathfunc::isnan $e]} { + # set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$low eq ""} { + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + + # } + # int { + # #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is integer -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {$low eq ""} { + # #lowside unspecified - check only high + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # #highside unspecified - check only low + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # #high and low specified + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # double { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is double -strict $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg_checks -typeranges]} { + # set ranges [dict get $thisarg_checks -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # #todo - small-value double comparisons with error-margin? review + # #todo - empty string for low or high + # set passed_checks 1 + # lassign $range low high + # if {$low$high ne ""} { + # if {$e_check < $low || $e_check > $high} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # bool { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is boolean -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # dict { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # puts "check_clausecolumn2 dict handler: c_idx:$c_idx clausecolumn:$clausecolumn clauseval_check:$clauseval_check" + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[llength $e_check] %2 != 0} { + # set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # } + # } + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set passed_checks 1 + # if {[tcl::dict::size $thisarg_checks]} { + # if {[dict exists $thisarg_checks -minsize]} { + # set minsizes [dict get $thisarg_checks -minsize] + # set e_check [lindex $clauseval_check $clausecolumn] + # set minsize [lindex $minsizes $clausecolumn] + # # -1 for disable is as good as zero + # if {[tcl::dict::size $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set e_check [lindex $clauseval_check $clausecolumn] + # set maxsize [lindex $maxsizes $clausecolumn] + # if {$maxsize ne "-1"} { + # if {[tcl::dict::size $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # alnum - + # alpha - + # ascii - + # control - + # digit - + # graph - + # lower - + # print - + # punct - + # space - + # upper - + # wordchar - + # xdigit { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is $type -strict $e_check]} { + # set e [lindex $clauseval $t] + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # file - + # directory - + # existingfile - + # existingdirectory { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + + # #//review - we may need '?' char on windows + # set passed_checks 1 + # if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + # #what about special file names e.g on windows NUL ? + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$type eq "existingfile"} { + # if {![file exists $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$type eq "existingdirectory"} { + # if {![file isdirectory $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # char { + # #review - char vs unicode codepoint vs grapheme? + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[tcl::string::length $e_check] != 1} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # tk_screen_units { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # switch -exact -- [string index $e_check end] { + # c - i - m - p { + # set numpart [string range $e_check 0 end-1] + # if {![tcl::string::is double $numpart]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # default { + # if {![tcl::string::is double $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # default { + # #default pass for unrecognised types - review. + # dict for {c_idx clauseval} $clauses_dict { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # foreach clauseresult $clause_results { + # if {[lsearch $clauseresult 1] == -1} { + # #no pass for this clause - fetch first? error and raise + # #todo - return error containing clause_indices so we can report more than one failing element at once? + # foreach e $clauseresult { + # if {[lindex $e 0] eq "errorcode"} { + # #errorcode msg values + #[para]ARGUMENTS: + #[list_begin arguments] + #[arg_def list-of-multiline-string deflist] + #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced + #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values + #[para]Each optionspec line defining a flag must be of the form: + #[para]-optionname -key val -key2 val2... + #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc + #[para]Each optionspec line defining a positional argument is of the form: + #[para]argumentname -key val -ky2 val2... + #[para]where the valid keys for each option specification are: -default -type -range -choices + #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value + #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. + #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. + #[arg_def list rawargs] + #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, + #but it could be a manually constructed list of values made for example from positional args defined in the proc. + #[list_end] + #[para] + + #consider line-processing example below for which we need info complete to determine record boundaries + #punk::args::get_dict [list { + # @opts + # -opt1 -default {} + # -opt2 -default { + # etc + # } + # @values -multiple 1 + #}] $args + + + + + #rawargs: args values to be parsed + #we take a definition list rather than resolved argspecs - because the definition could be dynamic + + #if definition has been seen before, + #define will either return a permanently cached argspecs (-dynamic 0) - or + # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. + set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] + + # ----------------------------------------------- + # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) + tcl::dict::with argspecs {} ;#turn keys into vars + #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names + # ----------------------------------------------- + set opt_form [dict get $proc_opts -form] + if {$opt_form eq "*"} { + set selected_forms $form_names + } elseif {[string is integer -strict $opt_form]} { + if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { + error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" + } + set selected_forms [list [lindex $form_names $opt_form]] + } else { + if {$opt_form ni $form_names} { + error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" + } + set selected_forms [list $opt_form] + } + + + #puts "-arg_info->$arg_info" + set flagsreceived [list] ;#for checking if required flags satisfied + set solosreceived [list] + set multisreceived [list] + #secondary purpose: + #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. + #-default value must not be appended to if argname not yet in flagsreceived + + + #todo: -minmultiple -maxmultiple ? + + # -- --- --- --- + # Handle leading positionals + # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? #todo - handle multiple fids? set fid [lindex $selected_forms 0] @@ -5053,20 +6863,18 @@ tcl::namespace::eval punk::args { if {$VAL_MIN eq ""} { set valmin 0 #set VAL_MIN 0 - foreach v $VAL_NAMES { - if {![dict get $ARG_INFO $v -optional]} { - # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) - # e.g -types {a ?xxx?} - #this has one required and one optional - set typelist [dict get $ARG_INFO $v -type] - set clause_length 0 - foreach t $typelist { - if {![string match {\?*\?} $t]} { - incr clause_length - } + foreach v $VAL_REQUIRED { + # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) + # e.g -types {a ?xxx?} + #this has one required and one optional + set typelist [dict get $ARG_INFO $v -type] + set clause_length 0 + foreach t $typelist { + if {![string match {\?*\?} $t]} { + incr clause_length } - incr valmin $clause_length } + incr valmin $clause_length } } else { set valmin $VAL_MIN @@ -5077,19 +6885,18 @@ tcl::namespace::eval punk::args { set argnames [tcl::dict::keys $ARG_INFO] #set optnames [lsearch -all -inline $argnames -*] #JJJ - set all_opts [list] set lookup_optset [dict create] foreach optset $OPT_NAMES { #optset e.g {-x|--longopt|--longopt=|--otherlongopt} - set optmembers [split $optset |] - foreach optdef $optmembers { + foreach optdef [split $optset |] { set opt [string trimright $optdef =] - if {$opt ni $all_opts} { + if {![dict exists $lookup_optset $opt]} { dict set lookup_optset $opt $optset - lappend all_opts $opt } } } + set all_opts [dict keys $lookup_optset] + set ridx 0 set rawargs_copy $rawargs set remaining_rawargs $rawargs @@ -5120,7 +6927,7 @@ tcl::namespace::eval punk::args { } #REVIEW - this attempt to classify leaders vs opts vs values doesn't account for leaders with clauses containing optional elements - #e.g @leadrs {x -type {int ?int?}} + #e.g @leaders {x -type {int ?int?}} set nameidx 0 if {$can_have_leaders} { if {$LEADER_TAKEWHENARGSMODULO} { @@ -5262,9 +7069,12 @@ tcl::namespace::eval punk::args { lappend leadernames_seen $leader_posn_name } dict incr leader_posn_names_assigned $leader_posn_name - for {set c 0} {$c < $consumed} {incr c} { - lappend pre_values [lpop remaining_rawargs 0] - } + #for {set c 0} {$c < $consumed} {incr c} { + # lappend pre_values [lpop remaining_rawargs 0] + #} + lappend pre_values {*}[lrange $remaining_rawargs 0 $consumed-1] + ledit remaining_rawargs 0 $consumed-1 + incr ridx $consumed incr ridx -1 ;#leave ridx at index of last r that we set } else { @@ -5295,6 +7105,12 @@ tcl::namespace::eval punk::args { } set end_leaders 0 + + #review - are we allowing multivalue leader clauses where the optional members are not at the tail? + #eg @leaders {double -type {?int? char}} + #as we don't type-check here while determining leaders vs opts vs values - this seems impractical. + #for consistency and simplification - we should only allow optional clause members at the tail + # and only for the last defined leader. This should be done in the definition parsing - not here. foreach t $leader_type { set raw [lindex $rawargs $ridx] if {[string match {\?*\?} $t] && [string match -* $raw]} { @@ -5307,11 +7123,30 @@ tcl::namespace::eval punk::args { break ;#break out of looking at -type members in the clause } else { #unrecognised flag - treat as value for optional member of the clause + #ridx must be valid if we matched -* - so lpop will succeed lappend pre_values [lpop remaining_rawargs 0] incr ridx } } else { - lappend pre_values [lpop remaining_rawargs 0] + if {[string match {\?*\?} $t]} { + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + set end_leaders 1 + break + } + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break + } + } else { + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break ;#let validation of required leaders report the error? + } + } incr ridx } } @@ -5392,12 +7227,6 @@ tcl::namespace::eval punk::args { #assert - remaining_rawargs has been reduced by leading positionals set opts [dict create] ;#don't set to OPT_DEFAULTS here - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> pre_values: $pre_values" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} set leaders [list] set arglist {} @@ -5425,30 +7254,6 @@ tcl::namespace::eval punk::args { break } set a [lindex $remaining_rawargs $i] - #if {$a eq "--"} { - # #REVIEW - # #remaining num args <= valmin already covered above - # if {$valmax != -1} { - # #finite max number of vals - # if {$remaining_args_including_this == $valmax} { - # #assume it's a value. - # set arglist [lrange $remaining_rawargs 0 $i-1] - # set post_values [lrange $remaining_rawargs $i end] - # } else { - # #assume it's an end-of-options marker - # lappend flagsreceived -- - # set arglist [lrange $remaining_rawargs 0 $i] - # set post_values [lrange $remaining_rawargs $i+1 end] - # } - # } else { - # #unlimited number of post_values accepted - # #treat this as eopts - we don't care if remainder look like options or not - # lappend flagsreceived -- - # set arglist [lrange $remaining_rawargs 0 $i] - # set post_values [lrange $remaining_rawargs $i+1 end] - # } - # break - #} switch -glob -- $a { -- { if {$a in $OPT_NAMES} { @@ -5468,18 +7273,18 @@ tcl::namespace::eval punk::args { if {$eposn > 2} { #only allow longopt-style = for double leading dash longopts #--*==0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { @@ -5698,25 +7532,25 @@ tcl::namespace::eval punk::args { if {[string match --* $a] && $eposn > 2} { #only allow longopt-style = for double leading dash longopts #--*= --x) + lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x) } else { if {[llength $OPT_NAMES]} { set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" @@ -5776,8 +7610,8 @@ tcl::namespace::eval punk::args { } } else { #not a flag/option - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] break } } @@ -5801,34 +7635,114 @@ tcl::namespace::eval punk::args { #} #--------------------------------------- + #Order the received options by the order in which they are *defined* + #EXCEPT that grouped options using same parsekey must be processed in received order set ordered_opts [dict create] - set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] - #unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) - # e.g -fg|-foreground - # e.g -x|--fullname= - #Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} - foreach o $unaliased_opts optset $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $optset]} { - #JJJ - set parsekey "" - if {[tcl::dict::exists $argstate $o -parsekey]} { - set parsekey [tcl::dict::get $argstate $o -parsekey] - } - if {$parsekey eq ""} { - set parsekey $o + + #set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] + ##unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) + ## e.g -fg|-foreground + ## e.g -x|--fullname= + ##Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} + #foreach o $unaliased_opts optset $OPT_NAMES { + # if {[dict exists $opts $o]} { + # dict set ordered_opts $o [dict get $opts $o] + # } elseif {[dict exists $OPT_DEFAULTS $optset]} { + # #JJJ + # set parsekey "" + # if {[tcl::dict::exists $argstate $o -parsekey]} { + # set parsekey [tcl::dict::get $argstate $o -parsekey] + # } + # if {$parsekey eq ""} { + # set parsekey $o + # } + # dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + # } + #} + + #puts ">>>>==== $opts" + set seen_pks [list] + #treating opts as list for this loop. + foreach optset $OPT_NAMES { + set parsekey "" + set has_parsekey_override 0 + if {[tcl::dict::exists $argstate $optset -parsekey]} { + set parsekey [tcl::dict::get $argstate $optset -parsekey] + } + if {$parsekey eq ""} { + set has_parsekey_override 0 + #fall back to last element of aliased option e.g -fg|-foreground -> "-foreground" + set parsekey [string trimright [lindex [split $optset |] end] =] + } else { + set has_parsekey_override 1 + } + lappend seen_pks $parsekey + set is_found 0 + set foundkey "" + set foundval "" + #no lsearch -stride avail in 8.6 + foreach {k v} $opts { + if {$k eq $parsekey} { + set foundkey $k + set is_found 1 + set foundval $v + #can be multiple - last match wins - don't 'break' out of foreach + } + } ;#avoiding further dict/list shimmering + #if {[dict exists $opts $parsekey]} { + # set found $parsekey + # set foundval [dict get $opts $parsekey] + #} + if {!$is_found && $parsekey ne $optset} { + #.g we may have in opts things like: -decreasing|-SORTDIRECTION -increasing|-SORTDIRECTION + #(where -SORTDIRECTION was configured as -parsekey) + #last entry must win + #NOTE - do not use dict for here. opts is not strictly a dict - dupe keys will cause wrong ordering + foreach {o v} $opts { + if {[string match *|$parsekey $o]} { + set foundkey $o + set is_found 1 + set foundval $v + #last match wins - don't 'break' out of foreach + } + } + } + if {$is_found} { + dict set ordered_opts $foundkey $foundval + } elseif {[tcl::dict::exists $OPT_DEFAULTS $optset]} { + if {$parsekey ne $optset} { + set tailopt [string trimright [lindex [split $optset |] end] =] + if {$tailopt ne $parsekey} { + #defaults for multiple options sharing a -parsekey value ? review + dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + } + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] } - dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] } } + #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval + #But make sure not to add any repeated parsekey e.g -increasing|-SORT -decreasing|-SORT + #use the seen_pks from the ordered_opts loop above + #keep working with opts only as list here.. + if {[llength $opts] > 2*[dict size $ordered_opts]} { + foreach {o o_val} $opts { + lassign [split $o |] _ parsekey ;#single pipe - 2 elements only | + if {$parsekey ne "" && $parsekey in $seen_pks} { + continue + } + if {![dict exists $ordered_opts $o]} { + dict set ordered_opts $o $o_val + } } } set opts $ordered_opts + #opts is a proper dict now + + #NOTE opts still may contain some entries in non-final form such as -flag|-PARSEKEY #--------------------------------------- @@ -5847,6 +7761,9 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + + set start_position $positionalidx set nameidx 0 #MAINTENANCE - (*nearly*?) same loop logic as for value @@ -5855,6 +7772,7 @@ tcl::namespace::eval punk::args { set ldr [lindex $leaders $ldridx] if {$leadername ne ""} { set leadertypelist [tcl::dict::get $argstate $leadername -type] + set leader_clause_size [llength $leadertypelist] set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] @@ -5887,11 +7805,17 @@ tcl::namespace::eval punk::args { } } - if {[llength $leadertypelist] == 1} { - set clauseval $ldr + if {$leader_clause_size == 1} { + #set clauseval $ldr + set clauseval [lindex $resultlist 0] } else { set clauseval $resultlist incr ldridx [expr {$consumed - 1}] + + #not quite right.. this sets the -type for all clauses - but they should run independently + #e.g if expr {} elseif 2 {script2} elseif 3 then {script3} (where elseif clause defined as "literal(elseif) expr ?literal(then)? script") + #the elseif 2 {script2} will raise an error because the newtypelist from elseif 3 then {script3} overwrote the newtypelist where then was given the type ?omitted-...? + tcl::dict::set argstate $leadername -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries } @@ -5904,12 +7828,15 @@ tcl::namespace::eval punk::args { #} if {$leadername in $leadernames_received} { tcl::dict::lappend leaders_dict $leadername $clauseval + tcl::dict::lappend argument_clause_typestate $leadername $newtypelist } else { tcl::dict::set leaders_dict $leadername [list $clauseval] + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] } set leadername_multiple $leadername } else { tcl::dict::set leaders_dict $leadername $clauseval + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] set leadername_multiple "" incr nameidx } @@ -5950,7 +7877,19 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- #satisfy test parse_withdef_leaders_no_phantom_default - foreach leadername [dict keys $leaders_dict] { + #foreach leadername [dict keys $leaders_dict] { + # if {[string is integer -strict $leadername]} { + # #ignore leadername that is a positionalidx + # #review - always trailing - could use break? + # continue + # } + # if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { + # #remove the name with empty-string default we used to establish fixed order of names + # #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. + # dict unset leaders_dict $leadername + # } + #} + dict for {leadername _v} $leaders_dict { if {[string is integer -strict $leadername]} { #ignore leadername that is a positionalidx #review - always trailing - could use break? @@ -5964,6 +7903,7 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- + set validx 0 set valname_multiple "" set valnames_received [list] @@ -5989,6 +7929,7 @@ tcl::namespace::eval punk::args { set val [lindex $values $validx] if {$valname ne ""} { set valtypelist [tcl::dict::get $argstate $valname -type] + set clause_size [llength $valtypelist] ;#common case is clause_size == 1 set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] @@ -6022,8 +7963,9 @@ tcl::namespace::eval punk::args { } #assert can_assign != 0, we have at least one value to assign to clause - if {[llength $valtypelist] == 1} { - set clauseval $val + if {$clause_size == 1} { + #set clauseval $val + set clauseval [lindex $resultlist 0] } else { #clauseval must contain as many elements as the max length of -types! #(empty-string/default for optional (?xxx?) clause members) @@ -6033,10 +7975,11 @@ tcl::namespace::eval punk::args { incr validx [expr {$consumed -1}] if {$validx > [llength $values]-1} { error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valtypelist] ] -argspecs $argspecs]] $msg + set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to $clause_size values." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength $clause_size ] -argspecs $argspecs]] $msg } + #incorrect - we shouldn't update the default. see argument_clause_typestate dict of lists of -type tcl::dict::set argstate $valname -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries } @@ -6049,17 +7992,21 @@ tcl::namespace::eval punk::args { #} if {$valname in $valnames_received} { tcl::dict::lappend values_dict $valname $clauseval + tcl::dict::lappend argument_clause_typestate $valname $newtypelist } else { tcl::dict::set values_dict $valname [list $clauseval] + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] } set valname_multiple $valname } else { tcl::dict::set values_dict $valname $clauseval + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect set valname_multiple "" incr nameidx } lappend valnames_received $valname } else { + #unnamed if {$valname_multiple ne ""} { set valtypelist [tcl::dict::get $argstate $valname_multiple -type] if {[llength $valname_multiple] == 1} { @@ -6108,6 +8055,10 @@ tcl::namespace::eval punk::args { } } #----------------------------------------------------- + #JJJJJJ + #if {[dict size $argument_clause_typestate]} { + # puts ">>>>>[dict get $argspecs id] typestate $argument_clause_typestate" + #} if {$leadermax == -1} { #only check min @@ -6146,6 +8097,7 @@ tcl::namespace::eval punk::args { } #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options + #(and may still contain non-final flag_ident entries such as -increasing|-SORTDIRECTION) #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) @@ -6215,21 +8167,58 @@ tcl::namespace::eval punk::args { #check types,ranges,choices set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" + #puts "get_dict>>>>>>>> ---opts_and_values:$opts_and_values" + #puts " >>>>>>> ---lookup_optset :$lookup_optset" #puts "---argstate:$argstate" - #JJJ api_argname e.g -increasing|-SORTOPTION - tcl::dict::for {api_argname value_group} $opts_and_values { - if {[string match -* $api_argname]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - if {[dict exists $lookup_optset $api_argname]} { - set argname [dict get $lookup_optset $api_argname] + #JJJ argname_or_ident; ident example: -increasing|-SORTOPTION + tcl::dict::for {argname_or_ident value_group} $opts_and_values { + # + #parsekey: key used in resulting leaders opts values dictionaries + # often distinct from the full argname in the ARG_INFO structure + # + if {[string match -* $argname_or_ident]} { + #ident format only applies to options/flags + if {[string first | $argname_or_ident] > -1} { + #flag_ident style (grouped fullname of option with -parsekey) + lassign [split $argname_or_ident |] fullflag parsekey ;#we expect only a single pipe in ident form | + if {[dict exists $lookup_optset $fullflag]} { + set argname [dict get $lookup_optset $fullflag] + #idents should already have correct parsekey + } else { + puts stderr "punk::args::get_dict unable to find $fullflag in $lookup_optset (parsekey:$parsekey) (value_group: $value_group)" + } } else { - puts stderr "punk::args::get_dict unable to find $api_argname in $lookup_optset (value_group: $value_group)" + if {[dict exists $lookup_optset $argname_or_ident]} { + #get full option name such as -fg|-foreground from non-alias name such as -foreground + #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined + set argname [dict get $lookup_optset $argname_or_ident] + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #default parsekey: last element in argname without trailing = + set parsekey [string trimright [lindex [split $argname |] end] =] + } + } else { + puts stderr "punk::args::get_dict unable to find $argname_or_ident in $lookup_optset (value_group: $value_group)" + } } } else { - set argname $api_argname + set argname $argname_or_ident + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #leader or value of form x|y has no special meaning and forms the parsekey in entirety by default. + set parsekey $argname + } } + #assert: argname is the key for the relevant argument info in the FORMS//ARG_INFO dict. (here each member available as $argstate) + #argname is usually the full name as specified in the definition: + #e.g -f|-path|--filename= + # (where the parsekey will be by default --filename, possibly overridden by -parsekey value) + #an example argname_or_compound for the above might be: -path|--filename + # where -path is the expanded form of the actual flag used (could have been for example just -p) and --filename is the parsekey set thisarg [tcl::dict::get $argstate $argname] #set thisarg_keys [tcl::dict::keys $thisarg] @@ -6243,22 +8232,55 @@ tcl::namespace::eval punk::args { set defaultval [tcl::dict::get $thisarg -default] } set typelist [tcl::dict::get $thisarg -type] + set clause_size [llength $typelist] set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 set validationtransform [tcl::dict::get $thisarg -validationtransform] #JJJJ + #if {$is_multiple} { + # set vlist $value_group + #} else { + # set vlist [list $value_group] + #} + ##JJJJ + #if {$clause_size == 1} { + # set vlist [list $vlist] + #} + + + #JJ 2025-07-25 + set vlist [list] + #vlist is a list of clauses. Each clause is a list of values of length $clause_size. + #The common case is clause_size 1 - but as we need to treat each clause as a list during validation - we need to list protect the clause when clause_size == 1. if {$is_multiple} { - set vlist $value_group + if {$clause_size == 1} { + foreach c $value_group { + lappend vlist [list $c] + } + } else { + set vlist $value_group + } } else { - set vlist [list $value_group] + if {$clause_size ==1} { + set vlist [list [list $value_group]] + } else { + set vlist [list $value_group] + } } - #JJJJ - if {[llength $typelist] == 1} { - set vlist [list $vlist] + set vlist_typelist [list] + if {[dict exists $argument_clause_typestate $argname]} { + #lookup saved newtypelist (argument_clause_typelist) from can_assign_value result where some optionals were given type ?omitted-? or ?defaulted-? + # args.test: parse_withdef_value_clause_missing_optional_multiple + set vlist_typelist [dict get $argument_clause_typestate $argname] + } else { + foreach v $vlist { + lappend vlist_typelist $typelist + } } + + + set vlist_original $vlist ;#retain for possible final strip_ansi #review - validationtransform @@ -6267,7 +8289,12 @@ tcl::namespace::eval punk::args { package require punk::ansi set vlist_check [list] foreach clause_value $vlist { - lappend vlist_check [punk::ansi::ansistrip $clause_value] + #lappend vlist_check [punk::ansi::ansistrip $clause_value] + set stripped [list] + foreach element $clause_value { + lappend stripped [punk::ansi::ansistrip $element] + } + lappend vlist_check $stripped } } else { #validate_ansistripped 0 @@ -6292,9 +8319,12 @@ tcl::namespace::eval punk::args { set argclass "Unknown argument" } } + set vlist_validate [list] + set vlist_check_validate [list] + set vlist_typelist_validate [list] #reduce our validation requirements by removing values which match defaultval or match -choices #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$api_argname in $receivednames && $has_choices} { + if {$parsekey in $receivednames && $has_choices} { #-choices must also work with -multiple #todo -choicelabels set choiceprefix [tcl::dict::get $thisarg -choiceprefix] @@ -6319,219 +8349,273 @@ tcl::namespace::eval punk::args { #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - set idx 0 ;# + set clause_index -1 ;# #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes #assert llength $vlist == llength [dict get $dname $argname] # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + foreach clause $vlist clause_check $vlist_check clause_typelist $vlist_typelist { + incr clause_index + set element_index -1 ;#element within clause - usually clause size is only 1 + foreach e $clause e_check $clause_check { + incr element_index + set allchoices_in_list 0 + if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { + #vlist and vlist_check can be list of lists if -multiple and -choicemultiple + #each e represents 0 or more choice selections + set c_list $e + set c_check_list $e_check + #todo? check if entire list matches default? + } else { + #only one choice at a time - ensure single entry in c_list c_check_list + set c_list [list $e] + set c_check_list [list $e_check] + } - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- + #----------------------------------- + #fast fail on the wrong number of choices + if {[llength $c_list] < $choicemultiple_min} { + set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg + } + if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { + set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg + } + #----------------------------------- - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail + set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list + foreach c $c_list c_check $c_check_list { + if {$nocase} { + set casemsg " (case insensitive)" + set choices_test [tcl::string::tolower $allchoices] + #Don't lcase the denylist - even in nocase mode! + #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] + set v_test [tcl::string::tolower $c_check] + } else { + set casemsg " (case sensitive)" + set choices_test $allchoices + set v_test $c_check + } + set choice_in_list 0 + set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? + if {!$matches_default} { + if {$choiceprefix} { + #can we handle empty string as a choice? It should just work - REVIEW/test + set choice_exact_match 0 + if {$c_check in $allchoices} { + #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing + set chosen $c_check + set choice_in_list 1 + set choice_exact_match 1 + } elseif {$v_test in $choices_test} { + #assert - if we're here, nocase must be true + #we know choice is present as full-length match except for case + #now we want to select the case from the choice list - not the supplied value + #we don't set choice_exact_match - because we will need to override the optimistic existing val below + #review + foreach avail [lsort -unique $allchoices] { + if {[string match -nocase $c $avail]} { + set chosen $avail + } } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] + #assert chosen will always get set + set choice_in_list 1 + } else { + #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" + #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. + #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. + #in this block we can treat empty result from prefix match as a non-match + if {$nocase} { + #nocase implies that our entered value doesn't have to match case of choices - + #but we would still like to select the best match if there are case-dups. + #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete + # selecting Del will find Delete, del will match delete (and raise error) + # but DEL will also match delete rather than Delete - so again an error is raised. + #This is counterintuitive with -nocase + #This is probably such an edge case that best served with documentation as a feature-not-bug + #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? + #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. + + set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] + #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing + set chosen [lsearch -inline -nocase $allchoices $chosen] + set choice_in_list [expr {$chosen ne ""}] + } else { + set chosen $bestmatch + set choice_in_list 1 + } } else { - set chosen $bestmatch - set choice_in_list 1 + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$chosen eq "" || $chosen in $choiceprefixreservelist} { + set choice_in_list 0 + } else { + set choice_in_list 1 + } } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { + #override choice_in_list if in deny list + #don't allow prefixing for elements from -choiceprefixdenylist + #we still use all elements to calculate the prefixes though + #review - case difference edge cases in choiceprefixdenylist !todo + if {$chosen in $choiceprefixdenylist} { set choice_in_list 0 - } else { - set choice_in_list 1 + set chosen "" } } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all + #override the optimistic existing val + #our existing values in $dname are not list-protected - so we need to check clause_size + if {$choice_in_list && !$choice_exact_match} { + set existing [tcl::dict::get [set $dname] $argname_or_ident] + if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { + #single choice allowed per clause-member + if {$is_multiple} { + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $element_index $chosen + } else { + lset existing $clause_index $element_index $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + #test: choice_multielement_clause + lset existing $element_index $chosen + tcl::dict::set $dname $argname_or_ident $existing + } } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing + if {$is_multiple} { + #puts ">>> existing $existing $choice_idx" + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $clause_index $choice_idx $chosen + } else { + lset existing $clause_index $element_index $choice_idx $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + lset existing $element_index $choice_idx $chosen + tcl::dict::set $dname $argname_or_ident $existing + } } } + } else { + #value as stored in $dname is ok + set choice_in_list [expr {$v_test in $choices_test}] } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] } - } - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" + if {!$choice_in_list && !$matches_default} { + if {!$choicerestricted} { + #if {$is_multiple} { + # set existing [tcl::dict::get [set $dname] $argname] + # lset existing $clause_index $v_test + # tcl::dict::set $dname $argname $existing + #} else { + # tcl::dict::set $dname $argname $v_test + #} + #JJJ + #lappend vlist_validate $c + #lappend vlist_check_validate $c_check } else { - set prefixmsg "" + #unhappy path + if {$choiceprefix} { + set prefixmsg " (or a unique prefix of a value)" + } else { + set prefixmsg "" + } + #review: $c vs $c_check for -badval? + set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg + #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname + } else { + #choice is in list or matches default - no validation for this specific element in the clause + lset clause_typelist $element_index any } + incr choice_idx } - incr choice_idx + + } ;#end foreach e in clause + #jjj 2025-07-16 + #if not all clause_typelist are 'any' + if {[lsearch -not $clause_typelist any] > -1} { + #at least one element still needs validation + lappend vlist_validate $clause + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } - incr idx - } + + } ;#end foreach clause in vlist + #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate + set vlist $vlist_validate + set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate } #todo - don't add to validation lists if not in receivednames - #if we have an optionset such as "-f|-x|-etc" api_argname is -etc - if {$api_argname ni $receivednames} { + #if we have an optionset such as "-f|-x|-etc"; the parsekey is -etc (unless it was overridden by -parsekey in definition) + if {$parsekey ni $receivednames} { set vlist [list] set vlist_check_validate [list] } else { if {[llength $vlist] && $has_default} { - #defaultval here is a value for the clause. - set vlist_validate [list] - set vlist_check_validate [list] - foreach clause_value $vlist clause_check $vlist_check { + #defaultval here is a value for the entire clause. (clause usually length 1) + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + set tp [dict get $thisarg -type] + set clause_size [llength $tp] + foreach clause_value $vlist clause_check $vlist_check clause_typelist $vlist_typelist { #JJJJ - #argname - #thisarg - set tp [dict get $thisarg -type] - if {[llength $tp] == 1} { - if {$clause_value ni $vlist_validate} { - #for -choicemultiple with default that could be a list use 'ni' ?? review + #REVIEW!!! we're inadvertently adding back in things that may have already been decided in choicelist loop as not requiring validation? + if {$clause_value ni $vlist_validate} { + if {$clause_size ==1} { + #for -choicemultiple with default that could be a list use 'ni' + #?? review! if {[lindex $clause_check 0] ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } - } - } else { - if {$clause_value ni $vlist_validate} { + } else { if {$clause_check ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } } } + #if {[llength $tp] == 1} { + # if {$clause_value ni $vlist_validate} { + # #for -choicemultiple with default that could be a list use 'ni' + # #?? review! + # if {[lindex $clause_check 0] ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} else { + # if {$clause_value ni $vlist_validate} { + # if {$clause_check ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} #Todo? #else ??? } set vlist $vlist_validate set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate } } @@ -6563,373 +8647,36 @@ tcl::namespace::eval punk::args { #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups #assert [llength $vlist] == [llength $vlist_check] if {[llength $vlist]} { - for {set t 0} {$t < [llength $typelist]} {incr t} { - set typespec [lindex $typelist $t] - set type [string trim $typespec ?] - #puts "$argname - switch on type: $type" - switch -- $type { - any {} - literal { - foreach clause_value $vlist { - set e [lindex $clause_value $t] - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - list { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } + #$t = clause column + + #for {set clausecolumn 0} {$clausecolumn < [llength $typelist]} {incr clausecolumn} {} + set clausecolumn -1 + foreach typespec $typelist { + incr clausecolumn + if {[dict exists $thisarg -typedefaults]} { + set tds [dict get $thisarg -typedefaults] + if {[lindex $vlist $clausecolumn] eq [lindex $tds $clausecolumn]} { + continue } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - if {[regexp [lindex $regexprepass $t] $e]} { - lappend pass_quick_list_e $clauseval - lappend pass_quick_list_e_check $clauseval_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach clauseval $remaining_e clauseval_check $remaining_e_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } + } - if {[tcl::dict::size $thisarg_checks]} { - foreach clauseval $remaining_e_check { - set e_check [lindex $clauseval $t] - if {[dict exists $thisarg_checks -minsize]} { - set minsize [dict get $thisarg_checks -minsize] - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign {} low high ;#set both empty - lassign $range low high - - if {"$low$high" ne ""} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - int { - #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign $range low high - if {"$low$high" ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - double { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is double -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -typeranges]} { - set ranges [dict get $thisarg_checks -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $range low high - if {$e_check < $low || $e_check > $high} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - bool { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsizes [dict get $thisarg_checks -minsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set minsize [lindex $minsizes $t] - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsizes [dict get $thisarg_checks -maxsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set maxsize [lindex $maxsizes $t] - if {$maxsize ne "-1"} { - if {[tcl::dict::size $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is $type -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[tcl::string::length $e_check] != 1} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } + set type_expression [string trim $typespec ?] + if {$type_expression in {any none}} { + continue } + #puts "$argname - switch on type_expression: $type_expression v:[lindex $vlist $clausecolumn]" + #set typespec [lindex $typelist $clausecolumn] + #todo - handle type-alternates e.g -type char|double + #------------------------------------------------------------------------------------ + #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #------------------------------------------------------------------------------------ + + + #todo - pass validation if matches an entry in -typedefaults + #has_typedefault? + #set typedefault [lindex $typedefaults $clausecolumn] + } @@ -6941,32 +8688,41 @@ tcl::namespace::eval punk::args { if {[tcl::dict::get $thisarg -multiple]} { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict $argname $stripped_list + tcl::dict::set leaders_dict $argname_or_ident $stripped_list } option { - tcl::dict::set opts $argname $stripped_list + tcl::dict::set opts $argname_or_ident $stripped_list } value { - tcl::dict::set values_dict $argname $stripped_list + tcl::dict::set values_dict $argname_or_ident $stripped_list } } } else { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] + tcl::dict::set leaders_dict $argname_or_ident [lindex $stripped_list 0] } option { - tcl::dict::set opts $argname [lindex $stripped_list 0] + tcl::dict::set opts $argname_or_ident [lindex $stripped_list 0] } value { - tcl::dict::set values_dict [lindex $stripped_list 0] + tcl::dict::set values_dict $argname_or_ident [lindex $stripped_list 0] } } } } } - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] + set finalopts [dict create] + dict for {o v} $opts { + if {[string first | $o] > -1} { + #set parsekey [lindex [split $o |] end] + dict set finalopts [lindex [split $o |] end] $v + } else { + dict set finalopts $o $v + } + } + return [tcl::dict::create leaders $leaders_dict opts $finalopts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] } lappend PUNKARGS [list { @@ -6991,6 +8747,33 @@ tcl::namespace::eval punk::args { return [list] } } + + + lappend PUNKARGS [list { + @id -id ::punk::args::eg + @cmd -name punk::args::eg\ + -summary\ + "Command examples."\ + -help\ + "Return command examples from -help in @examples + directive of a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + }] + proc eg {args} { + set argd [punk::args::parse $args withid ::punk::args::eg] + lassign [dict values $argd] leaders opts values received + set cmditems [dict get $values cmditem] + set id [lindex $cmditems 0] + set cmdargs [lrange $cmditems 1 end] + + set spec [get_spec $id] + if {$spec eq ""} { + return + } + return [dict get $spec examples_info -help] + } + lappend PUNKARGS [list { @id -id ::punk::args::synopsis @cmd -name punk::args::synopsis\ @@ -7114,26 +8897,63 @@ tcl::namespace::eval punk::args { set typelist [dict get $arginfo -type] if {[llength $typelist] == 1} { set tp [lindex $typelist 0] - if {[dict exists $arginfo -typesynopsis]} { + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { #set arg_display [dict get $arginfo -typesynopsis] - set clause [dict get $arginfo -typesynopsis] + set clause $ts } else { #set arg_display $argname - if {$tp eq "literal"} { - set clause [lindex $argname end] - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set clause $match - } else { - set clause $I$argname$NI + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + set type_alternatives [_split_type_expression $tp] + foreach tp_alternative $type_alternatives { + set firstword [lindex $tp_alternative 0] + switch -exact -- $firstword { + literal { + set match [lindex $tp_alternative 1] + lappend alternates $match + } + literalprefix { + #todo - trie styling on prefix calc + set match [lindex $tp_alternative 1] + lappend alternates $match + } + stringstartswith { + set match [lindex $tp_alternative 1] + lappend alternates $match* + } + stringendswith { + set match [lindex $tp_alternative 1] + lappend alternates *$match + } + default { + lappend alternates $I$argname$NI + } + } + + #if {$tp_alternative eq "literal"} { + # lappend alternates [lindex $argname end] + #} elseif {[string match literal(*) $tp_alternative]} { + # set match [string range $tp_alternative 8 end-1] + # lappend alternates $match + #} elseif {[string match literalprefix(*) $tp_alternative]} { + # set match [string range $tp_alternative 14 end-1] + # lappend alternates $match + #} else { + # lappend alternates $I$argname$NI + #} } + #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) + #todo - trie prefixes display + set alternates [punk::args::lib::lunique $alternates] + set clause [join $alternates |] } } else { set n [expr {[llength $typelist]-1}] set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types set clause "" - if {[dict exists $arginfo -typesynopsis]} { - set tp_displaylist [dict get $arginfo -typesynopsis] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts } else { set tp_displaylist [lrepeat [llength $typelist] ""] } @@ -7224,8 +9044,9 @@ tcl::namespace::eval punk::args { } - if {[dict exists $arginfo -typesynopsis]} { - set tp_display [dict get $arginfo -typesynopsis] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_display $ts #user may or may not have remembered to match the typesynopsis with the optionality by wrapping with ? #review - if user wrapped with ?*? and also leading/trailing ANSI - we won't properly strip #todo - enforce no wrapping '?*?' in define for -typesynopsis? @@ -7233,16 +9054,16 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_member [split $tp |] { + foreach tp_alternative [split $tp |] { #-type literal not valid for opt - review - if {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] + if {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] lappend alternates $match } else { - lappend alternates <$I$tp_member$NI> + lappend alternates <$I$tp_alternative$NI> } } #todo - trie prefixes display? @@ -7342,20 +9163,21 @@ tcl::namespace::eval punk::args { set typelist [dict get $arginfo -type] if {[llength $typelist] == 1} { set tp [lindex $typelist 0] - if {[dict exists $arginfo -typesynopsis]} { + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { #set arg_display [dict get $arginfo -typesynopsis] - set clause [dict get $arginfo -typesynopsis] + set clause $ts } else { #set arg_display $argname set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_member [split $tp |] { - if {$tp_member eq "literal"} { + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { lappend alternates [lindex $argname end] - } elseif {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] lappend alternates $match } else { lappend alternates $I$argname$NI @@ -7370,8 +9192,9 @@ tcl::namespace::eval punk::args { set n [expr {[llength $typelist]-1}] set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types set clause "" - if {[dict exists $arginfo -typesynopsis]} { - set tp_displaylist [dict get $arginfo -typesynopsis] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts } else { set tp_displaylist [lrepeat [llength $typelist] ""] } @@ -7387,14 +9210,14 @@ tcl::namespace::eval punk::args { } #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - foreach tp_member [split $tp |] { - if {$tp_member eq "literal"} { + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { lappend alternates $elementname - } elseif {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] lappend alternates $match } else { if {$td eq ""} { @@ -7602,6 +9425,26 @@ tcl::namespace::eval punk::args::lib { #[para] Secondary functions that are part of the API #[list_begin definitions] + #tcl86 compat for string is dict - but without -strict or -failindex options + if {[catch {string is dict {}} errM]} { + proc string_is_dict {args} { + #ignore opts + set str [lindex $args end] + if {[catch {[llength $str] len}]} { + return 0 + } + if {$len % 2 == 0} { + return 1 + } + return 0 + } + } else { + proc string_is_dict {args} { + string is dict {*}$args + } + } + + #proc utility1 {p1 args} { # #*** !doctools # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] @@ -8405,7 +10248,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.1.9 + set version 0.2 }] return diff --git a/src/bootsupport/modules/punk/config-0.1.tm b/src/bootsupport/modules/punk/config-0.1.tm index 3a5f25b0..8d5a5dca 100644 --- a/src/bootsupport/modules/punk/config-0.1.tm +++ b/src/bootsupport/modules/punk/config-0.1.tm @@ -449,7 +449,7 @@ tcl::namespace::eval punk::config { Accepts globs eg XDG*" @leaders -min 1 -max 1 #todo - load more whichconfig choices? - whichconfig -type string -choices {config startup-configuration running-configuration} + whichconfig -type any -choices {config startup-configuration running-configuration} @values -min 0 -max -1 globkey -type string -default * -optional 1 -multiple 1 }] @@ -495,7 +495,7 @@ tcl::namespace::eval punk::config { @cmd -name punk::config::configure -help\ "Get/set configuration values from a config" @leaders -min 1 -max 1 - whichconfig -type string -choices {defaults startup-configuration running-configuration} + whichconfig -type any -choices {defaults startup-configuration running-configuration} @values -min 0 -max 2 key -type string -optional 1 newvalue -optional 1 diff --git a/src/bootsupport/modules/punk/console-0.1.1.tm b/src/bootsupport/modules/punk/console-0.1.1.tm index 19d9d7e4..4322ceaa 100644 --- a/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/bootsupport/modules/punk/console-0.1.1.tm @@ -612,10 +612,12 @@ namespace eval punk::console { -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" - -expected_ms -default 100 -type integer -help\ + -expected_ms -default 300 -type integer -help\ "Expected number of ms for response from terminal. 100ms is usually plenty for a local terminal and a - basic query such as cursor position." + basic query such as cursor position. + However on a busy machine a higher timeout may be + prudent." @values -min 2 -max 2 query -type string -help\ "ANSI sequence such as \x1b\[?6n which @@ -680,19 +682,21 @@ namespace eval punk::console { upvar ::punk::console::ansi_response_timeoutid timeoutid set accumulator($callid) "" set waitvar($callid) "" - + lappend queue $callid if {[llength $queue] > 1} { #while {[lindex $queue 0] ne $callid} {} set queuedata($callid) $args set runningid [lindex $queue 0] - while {$runningid ne $callid} { + while {$runningid ne $callid} { + #puts stderr "." vwait ::punk::console::ansi_response_wait set runningid [lindex $queue 0] if {$runningid ne $callid} { set ::punk::console::ansi_response_wait($runningid) $::punk::console::ansi_response_wait($runningid) update ;#REVIEW - probably a bad idea after 10 + set runningid [lindex $queue 0] ;#jn test } } } @@ -779,7 +783,7 @@ namespace eval punk::console { puts "blank extension $waitvar($callid)" puts "->[set $waitvar($callid)]<-" } - puts stderr "get_ansi_response_payload Extending timeout by $extension" + puts stderr "get_ansi_response_payload Extending timeout by $extension for callid:$callid" after cancel $timeoutid($callid) set total_elapsed [expr {[clock millis] - $tslaunch($callid)}] set last_elapsed [expr {[clock millis] - $lastvwait}] @@ -916,7 +920,8 @@ namespace eval punk::console { unset -nocomplain tslaunch($callid) dict unset queuedata $callid - lpop queue 0 + #lpop queue 0 + ledit queue 0 0 if {[llength $queue] > 0} { set next_callid [lindex $queue 0] set waitvar($callid) go_ahead @@ -977,7 +982,7 @@ namespace eval punk::console { set tsnow [clock millis] set total_elapsed [expr {[set tslaunch($callid)] - $tsnow}] set last_elapsed [expr {[set tsclock($callid)] - $tsnow}] - if {[string length $chunks($callid)] % 10 == 0 || $last_elapsed > 16} { + if {[string length $sofar] % 10 == 0 || $last_elapsed > 16} { if {$total_elapsed > 3000} { #REVIEW #too long since initial read handler launched.. @@ -1239,7 +1244,7 @@ namespace eval punk::console { lappend PUNKARGS [list { @id -id ::punk::console::show_input_response @cmd -name punk::console::show_input_response -help\ - "" + "Debug command for console queries using ANSI" @opts -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" @@ -1247,9 +1252,9 @@ namespace eval punk::console { "Number of ms to wait for response" @values -min 1 -max 1 request -type string -help\ - "ANSI sequence such as \x1b\[?6n which + {ANSI sequence such as \x1b\[?6n which should elicit a response by the terminal - on stdin" + on stdin} }] proc show_input_response {args} { set argd [punk::args::parse $args withid ::punk::console::show_input_response] diff --git a/src/bootsupport/modules/punk/du-0.1.0.tm b/src/bootsupport/modules/punk/du-0.1.0.tm index 7d1375d7..a95a6242 100644 --- a/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/bootsupport/modules/punk/du-0.1.0.tm @@ -70,6 +70,7 @@ namespace eval punk::du { proc du { args } { variable has_twapi + #todo - use punk::args if 0 { switch -exact [llength $args] { diff --git a/src/bootsupport/modules/punk/lib-0.1.2.tm b/src/bootsupport/modules/punk/lib-0.1.2.tm index 5532ed33..6ce76618 100644 --- a/src/bootsupport/modules/punk/lib-0.1.2.tm +++ b/src/bootsupport/modules/punk/lib-0.1.2.tm @@ -301,6 +301,7 @@ tcl::namespace::eval punk::lib::compat { if {"::lpop" ne [info commands ::lpop]} { #puts stderr "Warning - no built-in lpop" interp alias {} lpop {} ::punk::lib::compat::lpop + punk::args::set_alias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore } proc lpop {lvar args} { #*** !doctools @@ -339,6 +340,51 @@ tcl::namespace::eval punk::lib::compat { set l $newlist return $v } + if {"::ledit" ni [info commands ::ledit]} { + interp alias {} ledit {} ::punk::lib::compat::ledit + punk::args::set_alias ::punk::lib::compat::ledit ::ledit + } + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [punk::lib::lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [punk::lib::lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } #slight isolation - varnames don't leak - but calling context vars can be affected @@ -695,14 +741,15 @@ namespace eval punk::lib { proc lswap {lvar a z} { upvar $lvar l - if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { + set len [llength $l] + if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { #lindex_resolve_basic returns only -1 if out of range #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred #(e.g using: lswap mylist end-2 end on a two element list) #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $l $a] + set a_index [lindex_resolve $len $a] set a_msg "" switch -- $a_index { -2 { @@ -712,7 +759,7 @@ namespace eval punk::lib { set a_msg "1st supplied index $a is below the lower bound for the list (0)" } } - set z_index [lindex_resolve $l $z] + set z_index [lindex_resolve $len $z] set z_msg "" switch -- $z_index { -2 { @@ -1100,7 +1147,7 @@ namespace eval punk::lib { - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segement in each pattern switches to a dict operation to retrieve the value by key. + The second level segment in each pattern switches to a dict operation to retrieve the value by key. When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. } }] @@ -1137,11 +1184,13 @@ namespace eval punk::lib { if {!$has_punk_ansi} { set RST "" set sep " = " - set sep_mismatch " mismatch " + #set sep_mismatch " mismatch " + set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) } else { set RST [punk::ansi::a] set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " } package require punk::pipe #package require punk ;#we need pipeline pattern matching features @@ -1173,6 +1222,7 @@ namespace eval punk::lib { -keysortdirection -default increasing -choices {increasing decreasing} -debug -default 0 -type boolean -help\ "When enabled, produces some rudimentary debug output on stderr" + -- -type none -optional 1 @values -min 1 -max -1 dictvalue -type list -help\ "dict or list value" @@ -1465,7 +1515,7 @@ namespace eval punk::lib { if {![regexp $re_idxdashidx $p _match a b]} { error "unrecognised pattern $p" } - set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high + set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds if {${lower_resolve} == -2} { ##x @@ -1478,7 +1528,7 @@ namespace eval punk::lib { } else { set lower $lower_resolve } - set upper [punk::lib::lindex_resolve $dval $b] + set upper [punk::lib::lindex_resolve [llength $dval] $b] if {$upper == -3} { ##x #upper bound is below list range - @@ -1831,7 +1881,8 @@ namespace eval punk::lib { if {$last_hidekey} { append result \n } - append result [textblock::join_basic -- $kblock $sblock $vblock] \n + #append result [textblock::join_basic -- $kblock $sblock $vblock] \n + append result [textblock::join_basic_raw $kblock $sblock $vblock] \n } set last_hidekey $hidekey incr kidx @@ -1880,6 +1931,19 @@ namespace eval punk::lib { } proc is_list_all_in_list {small large} { + if {[llength $small] > [llength $large]} {return 0} + foreach x $large { + ::set ($x) {} + } + foreach x $small { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } + #v2 generally seems slower + proc is_list_all_in_list2 {small large} { set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] return [struct::list equal [lsort $small] $small_in_large] } @@ -1888,11 +1952,22 @@ namespace eval punk::lib { package require struct::list package require struct::set } - append body [info body is_list_all_in_list] - proc is_list_all_in_list {small large} $body + append body [info body is_list_all_in_list2] + proc is_list_all_in_list2 {small large} $body } - proc is_list_all_ni_list {a b} { + proc is_list_all_ni_list {A B} { + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {[info exists ($x)]} { + return 0 + } + } + return 1 + } + proc is_list_all_ni_list2 {a b} { set i [struct::set intersect $a $b] return [expr {[llength $i] == 0}] } @@ -1900,8 +1975,8 @@ namespace eval punk::lib { set body { package require struct::list } - append body [info body is_list_all_ni_list] - proc is_list_all_ni_list {a b} $body + append body [info body is_list_all_ni_list2] + proc is_list_all_ni_list2 {a b} $body } #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist @@ -1917,7 +1992,16 @@ namespace eval punk::lib { } return $result } + #with ledit (also avail in 8.6 using punk::lib::compat::ledit proc ldiff2 {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + foreach item $removeitems { + set posns [lsearch -all -exact $fromlist $item] + foreach p $posns {ledit fromlist $p $p} + } + return $fromlist + } + proc ldiff3 {fromlist removeitems} { set doomed [list] foreach item $removeitems { lappend doomed {*}[lsearch -all -exact $fromlist $item] @@ -2158,35 +2242,75 @@ namespace eval punk::lib { } } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bunds on upper vs lower side - proc lindex_resolve {list index} { + # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side + #REVIEW: This shouldn't really need the list itself - just the length would suffice + punk::args::define { + @id -id ::punk::lib::lindex_resolve + @cmd -name punk::lib::lindex_resolve\ + -summary\ + "Resolve an indexexpression to an integer based on supplied list or string length."\ + -help\ + "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 + to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating + whether the index was below or above the range of possible indices for the length supplied. + + Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. + This means the proc may be called with something like $x+2 end-$y etc + Sometimes the actual integer index is desired. + + We want to resolve the index used, without passing arbitrary expressions into the 'expr' function + - which could have security risks. + lindex_resolve will parse the index expression and return: + a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) + b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) + lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote + out of range at either end of the list/string. + Otherwise it will return an integer corresponding to the position in the data. + This is in stark contrast to Tcl list/string function indices which will return empty strings for out of + bounds indices, or in the case of lrange, return results anyway. + Like Tcl list commands - it will produce an error if the form of the index is not acceptable. + For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side + - thus returning -2 + + Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. + We will get something like 10+1 - which can be resolved safely with expr + " + @values -min 2 -max 2 + datalength -type integer + index -type indexexpression + } + proc lindex_resolve {len index} { #*** !doctools - #[call [fun lindex_resolve] [arg list] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list - #[para]Users may define procs which accept a list index and wish to accept the forms understood by Tcl. + #[call [fun lindex_resolve] [arg len] [arg index]] + #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length + #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. #[para]This means the proc may be called with something like $x+2 end-$y etc #[para]Sometimes the actual integer index is desired. #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. #[para]lindex_resolve will parse the index expression and return: #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list + #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out or bounds indices, or in the case of lrange, return results anyway. + #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which we will resolve (hopefully safely) with expr + #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr #if {![llength $list]} { # #review # return ??? #} + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } else { #integer may still have + sign - normalize with expr @@ -2203,7 +2327,7 @@ namespace eval punk::lib { } } else { #index is 'end' - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { #special case - 'end' with empty list - treat end like a positive number out of bounds return -2 @@ -2212,7 +2336,7 @@ namespace eval punk::lib { } } if {$offset == 0} { - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { return -2 ;#special case as above } else { @@ -2220,7 +2344,7 @@ namespace eval punk::lib { } } else { #by now, if op = + then offset = 0 so we only need to handle the minus case - set index [expr {([llength $list]-1) - $offset}] + set index [expr {($len-1) - $offset}] } if {$index < 0} { return -3 @@ -2245,33 +2369,32 @@ namespace eval punk::lib { } if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } return $index } } } - proc lindex_resolve_basic {list index} { + proc lindex_resolve_basic {len index} { #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] + #[call [fun lindex_resolve_basic] [arg len] [arg index]] #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) #[para] returns -1 for out of range at either end, or a valid integer index #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command + #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 #[para] For pure integer indices the performance should be equivalent - #set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ - # - which - #for {set i 0} {$i < [llength $list]} {incr i} { - # lappend indices $i - #} + if {![string is integer -strict $len]} { + error "lindex_resolve_basic len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { + if {$index < 0 || ($index >= $len)} { #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. return -1 } else { @@ -2279,13 +2402,15 @@ namespace eval punk::lib { return [expr {$index}] } } - if {[llength $list]} { - set indices [punk::lib::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) + if {$len > 0} { + #For large len - this is a wasteful allocation if no true lseq available in Tcl version. + #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) + set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. } else { - set indices [list] + set testlist [list] + #we want to call 'lindex' even in this case - to get the appropriate error message } - set idx [lindex $indices $index] + set idx [lindex $testlist $index] if {$idx eq ""} { #we have no way to determine if out of bounds is at lower vs upper end return -1 @@ -2304,6 +2429,81 @@ namespace eval punk::lib { } } + proc string_splitbefore {str index} { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + return [list $str ""] + } + -3 { + return [list "" $str] + } + } + } + return [list [string range $str 0 $index-1] [string range $str $index end]] + #scan %s stops at whitespace - not useful here. + #scan $s %${p}s%s + } + proc string_splitbefore_indices {str args} { + set parts [list $str] + set sizes [list [string length $str]] + set s 0 + foreach index $args { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + -3 { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + } + } + if {$index <= 0} { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + if {$index >= [string length $str]} { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + set i -1 + set a 0 + foreach sz $sizes { + incr i + if {$a + $sz > $index} { + set p [lindex $parts $i] + #puts "a:$a index:$index" + if {$a == $index} { + break + } + ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] + ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] + break + } + incr a $sz + } + #puts "->parts:$parts" + #puts "->sizes:$sizes" + } + return $parts + } proc K {x y} {return $x} #*** !doctools @@ -3133,8 +3333,7 @@ namespace eval punk::lib { #Each resulting line should have a reset of some type at start and a pure-reset at end to stop #see if we can find an ST sequence that most terminals will not display for marking sections? if {$opt_ansireplays} { - #package require punk::ansi - + ;#package require punk::ansi if {$opt_ansiresets} { set RST "\x1b\[0m" } else { diff --git a/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/bootsupport/modules/punk/libunknown-0.1.tm index 6f01e340..19d5177d 100644 --- a/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -81,14 +81,15 @@ tcl::namespace::eval punk::libunknown { }] variable epoch - if {![info exists epoch]} { - set tmstate [dict create 0 {}] - set pkgstate [dict create 0 {}] - set tminfo [dict create current 0 epochs $tmstate] - set pkginfo [dict create current 0 epochs $pkgstate] + #if {![info exists epoch]} { + # set tmstate [dict create 0 {}] + # set pkgstate [dict create 0 {}] + # set tminfo [dict create current 0 epochs $tmstate] + # set pkginfo [dict create current 0 epochs $pkgstate] + + # set epoch [dict create tm $tminfo pkg $pkginfo] + #} - set epoch [dict create tm $tminfo pkg $pkginfo] - } variable has_package_files if {[catch {package files foobaz}]} { @@ -114,7 +115,19 @@ tcl::namespace::eval punk::libunknown { # Import the pattern used to check package names in detail. variable epoch set pkg_epoch [dict get $epoch tm current] - + set must_scan 0 + if {[dict exists $epoch tm untracked $name]} { + set must_scan 1 + #a package that was in the package database at the start - is now being searched for as unknown + #our epoch info is not reliable for pre-known packages - so increment the epoch and fully clear the 'added' paths even in zipfs to do proper scan + + #review + #epoch_incr_pkg clearadded + #epoch_incr_tm clearadded + #puts ">>>> removing untracked tm: $name" + dict unset epoch tm untracked $name + #whie it is not the most common configuration - a package could be provided both as a .tm and by packageIndex.tcl files + } #variable paths upvar ::tcl::tm::paths paths @@ -151,7 +164,8 @@ tcl::namespace::eval punk::libunknown { if {![interp issafe] && ![file exists $path]} { continue } - set currentsearchpath [file join $path $pkgroot] + set currentsearchpath $path + set specificsearchpath [file join $path $pkgroot] # Get the module files out of the subdirectories. # - Safe Base interpreters have a restricted "glob" command that @@ -162,32 +176,35 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $path]} { - if {[dict exists $epoch tm epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch tm epochs $pkg_epoch indexes $specificsearchpath]} { #indexes are actual .tm files here - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles ( cached ): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" } else { - if {![interp issafe] && ![file exists $currentsearchpath]} { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + if {![interp issafe] && ![file exists $specificsearchpath]} { + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] continue } - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] # ################################################################# if {$has_zipfs && [string match $zipfsroot* $path]} { + #The entire tm tre is available so quickly from the zipfs::list call - that we can gather all at once. set tmfiles [::tcl::zipfs::list $currentsearchpath/*.tm] ;#could theoretically be a dir - this is effectively a tree traversal foreach tm_path $tmfiles { dict set epoch tm epochs $pkg_epoch indexes [file dirname $tm_path] $tm_path $pkg_epoch } - #retrieval using tcl::zipfs::list got (and cached) extras - limit to currentsearchpath - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + #retrieval using tcl::zipfs::list got (and cached) extras - limit to specificsearchpath + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] } else { - set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + #set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + set tmfiles [glob -nocomplain -directory $specificsearchpath *.tm] foreach tm_path $tmfiles { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + #dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath $tm_path $pkg_epoch } } #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles (UNcached): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" @@ -203,8 +220,8 @@ tcl::namespace::eval punk::libunknown { set can_skip_update 0 if {[string match $zipfsroot* $path]} { #static tm location - if {[dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath $name]} { + if {[dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath]} { + if {![dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath $name]} { #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP orig:$original name:$name args:$args searchpath:$currentsearchpath" set can_skip_update 1 @@ -213,19 +230,13 @@ tcl::namespace::eval punk::libunknown { #dict unset epoch tm epochs $pkg_epoch added $currentsearchpath $name } } - } else { - #dynamic - can only skip if negatively cached for the current epoch - if {[dict exists $epoch tm epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_update 1 - } - } + if {!$can_skip_update} { set strip [llength [file split $path]] set found_name_in_currentsearchpath 0 ;#for negative cache by epoch - catch { + if {[catch { foreach file $tmfiles { set pkgfilename [join [lrange [file split $file] $strip end] ::] @@ -252,6 +263,20 @@ tcl::namespace::eval punk::libunknown { # the one we already have. # This does not apply to Safe Base interpreters because # the token-to-directory mapping may have changed. + + #JMN - review. + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } + if {$pkgname eq $name} { + #can occur multiple times, different versions + #record package name as found in this path whether version satisfies or not + set found_name_in_currentsearchpath 1 + } + #don't override the ifneeded script - for tm files the first encountered 'wins'. continue } @@ -273,8 +298,15 @@ tcl::namespace::eval punk::libunknown { "[::list package provide $pkgname $pkgversion];[::list source $file]" #JMN - #store only once for each name, although there may be multiple versions - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkg_epoch + #store only once for each name, although there may be multiple versions of same package within this searchpath + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. + #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } # We abort in this unknown handler only if we got a # satisfying candidate for the requested package. @@ -298,10 +330,8 @@ tcl::namespace::eval punk::libunknown { set found_name_in_currentsearchpath 1 } } - } - if {!$found_name_in_currentsearchpath} { - #can record as unfound for this path - for this epoch - dict set epoch tm epochs $pkg_epoch unfound $currentsearchpath $name 1 + } errMsg]} { + puts stderr "zipfs_tm_Unknownhandler: error for tm file $file searchpath:$currentsearchpath" } } @@ -380,9 +410,9 @@ tcl::namespace::eval punk::libunknown { } - if {$satisfied} { - ##return - } + #if {$satisfied} { + # return + #} } # Fallback to previous command, if existing. See comment above about @@ -399,6 +429,25 @@ tcl::namespace::eval punk::libunknown { variable epoch set pkg_epoch [dict get $epoch pkg current] + #review - the ifneeded script is not the only thing required in a new interp.. consider tclIndex files and auto_load mechanism. + #also the pkgIndex.tcl could possibly provide a different ifneeded script based on interp issafe (or other interp specific things?) + #if {[dict exists $epoch scripts $name]} { + # set vscripts [dict get $epoch scripts $name] + # dict for {v scr} $vscripts { + # puts ">package ifneeded $name $v" + # package ifneeded $name $v $scr + # } + # return + #} + set must_scan 0 + if {[dict exists $epoch pkg untracked $name]} { + #a package that was in the package database at the start - is now being searched for as unknown + #(due to a package forget?) + #our epoch info is not valid for pre-known packages - so setting must_scan to true + set must_scan 1 + #puts ">>>> removing pkg untracked: $name" + dict unset epoch pkg untracked $name + } #global auto_path env global auto_path @@ -414,7 +463,7 @@ tcl::namespace::eval punk::libunknown { set zipfsroot [tcl::zipfs::root] set has_zipfs 1 } else { - set zipfsroot "//zipfs:/" ;#doesn't matter much what we use here - don't expect in tm list if no zipfs commands + set zipfsroot "//zipfs:/" ;#doesn't matter too much what we use here - don't expect in tm list if no zipfs commands set has_zipfs 0 } @@ -425,6 +474,14 @@ tcl::namespace::eval punk::libunknown { #question is whether some pkgIndex.tcl files may do a package forget? They probably don't/shouldn't(?) Does that matter here anyway? set before_dict [dict create] + #J2 + #siblings that have been affected by source scripts - need to retest ifneeded scripts at end for proper ordering. + set refresh_dict [dict create] + + #Note that autopath is being processed from the end to the front + #ie last lappended first. This means if there are duplicate versions earlier in the list, + #they will be the last to call 'package provide' for that version and so their provide script will 'win'. + #This means we should have faster filesystems such as zipfs earlier in the list. # Cache the auto_path, because it may change while we run through the # first set of pkgIndex.tcl files @@ -432,6 +489,7 @@ tcl::namespace::eval punk::libunknown { while {[llength $use_path]} { set dir [lindex $use_path end] + # Make sure we only scan each directory one time. if {[info exists tclSeenPath($dir)]} { set use_path [lrange $use_path 0 end-1] @@ -449,7 +507,7 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $dir]} { set currentsearchpath $dir - if {[dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { set indexfiles [dict keys [dict get $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]] #puts stderr "--->zipfs_tclPkgUnknown $tid llength tmfiles ( cached ): [format %4d [llength $indexfiles]] name:$name searchpath:$currentsearchpath" } else { @@ -468,29 +526,26 @@ tcl::namespace::eval punk::libunknown { } set can_skip_sourcing 0 - if {$has_zipfs && [string match $zipfsroot* $dir]} { + #if {$has_zipfs && [string match $zipfsroot* $dir]} { #static auto_path dirs - #can avoid scan if added via this path in any epoch - if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" - set can_skip_sourcing 1 - } else { - #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? - #remove it and let it be readded if it's still provided by this path? - #probably doesn't make sense for static path? - #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + if {!$must_scan} { + if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { + if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { + #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. + #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" + set can_skip_sourcing 1 + } else { + #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? + #remove it and let it be readded if it's still provided by this path? + #probably doesn't make sense for static path? + #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + } } } - } else { - #dynamic auto_path dirs - libs could have been added/removed - #scan unless cached negative for this epoch - if {[dict exists $epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_sourcing 1 - } - } + #} + + + #An edge case exception is that after a package forget, a deliberate call to 'package require non-existant' #will not trigger rescans for all versions of other packages. #A rescan of a specific package for all versions can still be triggered with a package require for @@ -498,33 +553,53 @@ tcl::namespace::eval punk::libunknown { #(or misordered min max e.g package require md5 1-0 i.e a deliberately unsatisfiable version range) set sourced 0 + set just_added [dict create] + set just_changed [dict create] + #set sourced_files [list] + + #J2 + #set can_skip_sourcing 0 + if {!$can_skip_sourcing} { #Note - naive comparison of before_pkgs vs after_pkgs isn't quite enough to tell if something was added. It could have added a version. #this will stop us rescanning everything properly by doing a 'package require nonexistant' - #use 'info exists' to only call package names once and then append? worth it? + #use 'info exists' to only call package names once and then append? + #This could be problematic? (re-entrant tclPkgUnknown in some pkgIndex scripts?) pkgIndex.tcl scripts "shouldn't" do this? if {![info exists before_pkgs]} { set before_pkgs [package names] + #update the before_dict which persists across while loop + #we need to track the actual 'ifneeded' script not just version numbers, + #because the last ifneeded script processed for each version is the one that ultimately applies. + foreach bp $before_pkgs { + #dict set before_dict $bp [package versions $bp] + foreach v [package versions $bp] { + dict set before_dict $bp $v [package ifneeded $bp $v] + } + } } - #update the before_dict which persists across while loop - foreach bp $before_pkgs { - dict set before_dict $bp [package versions $bp] - } - catch { + #set before_pkgs [package names] + + #catch { foreach file $indexfiles { set dir [file dirname $file] if {![info exists procdDirs($dir)]} { try { - #puts stderr "----->0 sourcing $file" + #if {[string match //zipfs*registry* $file]} { + # puts stderr "----->0 sourcing zipfs file $file" + #} incr sourced ;#count as sourced even if source fails; keep before actual source action #::tcl::Pkg::source $file + #lappend sourced_files $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (1)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (1)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" @@ -532,8 +607,11 @@ tcl::namespace::eval punk::libunknown { set procdDirs($dir) 1 } } + #each source operation could affect auto_path - and thus increment the pkg epoch (via trace on ::auto_path) + #e.g tcllib pkgIndex.tcl appends to auto_path + set pkg_epoch [dict get $epoch pkg current] } - } + #} set dir [lindex $use_path end] if {![info exists procdDirs($dir)]} { set file [file join $dir pkgIndex.tcl] @@ -542,20 +620,24 @@ tcl::namespace::eval punk::libunknown { try { #puts "----->2 sourcing $file" incr sourced + #lappend sourced_files $file #::tcl::Pkg::source $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (2)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (2)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" } on ok {} { set procdDirs($dir) 1 } + set pkg_epoch [dict get $epoch pkg current] } } #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath [dict create] @@ -568,34 +650,89 @@ tcl::namespace::eval punk::libunknown { } set after_pkgs [package names] - set just_added [dict create] + #puts "@@@@pkg epochs $pkg_epoch searchpath:$currentsearchpath name:$name before: [llength $before_pkgs] after: [llength $after_pkgs]" if {[llength $after_pkgs] > [llength $before_pkgs]} { foreach a $after_pkgs { - if {![dict exists $before_dict $a]} { - dict set just_added $a 1 - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $pkg_epoch + foreach v [package versions $a] { + if {![dict exists $before_dict $a $v]} { + dict set just_added $a $v 1 + set iscript [package ifneeded $a $v] + #J2 + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a [dict create e $pkg_epoch v $v] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $v [dict create e $pkg_epoch scr $iscript] + if {$must_scan} { + dict unset epoch pkg untracked $a + } + } } } - #puts stderr ">>>zipfs_tclPkgUnknown added [llength $added_pkgs]" - #puts stderr ">>> [join [lrange $added_pkgs 0 10] \n]..." } - dict for {bp bpversions} $before_dict { - if {[dict exists $just_added $bp]} { - continue - } - if {[llength $bpversions] != [llength [package versions $bp]]} { - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $pkg_epoch + + #----------------- + #if {[dict size $just_added]} { + # puts stderr "\x1b\[31m>>>zipfs_tclPkgUnknown called on name:$name added [dict size $just_added] from searchpath:$currentsearchpath\x1b\[m" + # puts stderr ">>> [join [lrange [dict keys $just_added] 0 10] \n]..." + #} else { + # tclLog ">>>zipfs_tclPkgUnknown called on name:$name Nothing added for searchpath:$currentsearchpath" + # if {[string match twapi* $name]} { + # tclLog ">>>zipfs_tclPkgUnknown: sourced_files:" + # foreach f $sourced_files { + # puts ">>> $f" + # } + # } + # if {$currentsearchpath in "//zipfs:/app //zipfs:/app/tcl_library"} { + # puts " before_pkgs: [llength $before_pkgs]" + # puts " lsearch msgcat: [lsearch $before_pkgs msgcat]" + # puts " after_pkgs: [llength $after_pkgs]" + # puts " \x1b\31mlsearch msgcat: [lsearch $after_pkgs msgcat]\x1b\[m" + # if {[lsearch $after_pkgs msgcat] >=0} { + # set versions [package versions msgcat] + # puts "msgcat versions: $versions" + # foreach v $versions { + # puts "\x1b\[32m $v ifneeded: [package ifneeded msgcat $v] \x1b\[m" + # } + # } + # } + #} + #----------------- + + #review - just because this searchpath didn't add a package or add a version for the package + #it doesn't mean there wasn't a version of this package supplied there + #It may just be the same version as one we've already found. + #The last one found (earlier in auto_path) for a version is the one that supplies the final 'package provide' statement (by overriding it) + # + dict for {bp bpversionscripts} $before_dict { + #if {!$must_scan && ![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $bp]} { + # #puts -nonewline . + # continue + #} + dict for {bv bscript} $bpversionscripts { + set nowscript [package ifneeded $bp $bv] + if {$bscript ne $nowscript} { + #ifneeded script has changed. The same version of bp was supplied on this path. + #As it's processed later - it will be the one in effect. + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp [dict create e $pkg_epoch v $bv] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $bv [dict create e $pkg_epoch scr $nowscript] + dict set before_dict $bp $bv $nowscript + dict set just_changed $bp $bv 1 + #j2 + if {$must_scan} { + dict unset epoch pkg untracked $bp + } + } } } - #puts stderr "zipfs_tclPkgUnknown $tid sourced: $sourced (under path: $currentsearchpath)" - if {$name ni $after_pkgs} { - #cache negative result (for this epoch only) - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } elseif {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } - lappend before_pkgs {*}[dict keys $just_added] + #update before_pkgs & before_dict for next path + dict for {newp vdict} $just_added { + if {$newp ni $before_pkgs} { + lappend before_pkgs $newp + } + dict for {v _} $vdict { + set nowscript [package ifneeded $newp $v] + dict set before_dict $newp $v $nowscript + } + } } } @@ -680,20 +817,143 @@ tcl::namespace::eval punk::libunknown { } } set old_path $auto_path + + dict for {pkg versions} $just_changed { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + dict for {pkg versions} $just_added { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + } + + #refresh ifneeded scripts for just_added/just_changed + #review: searchpaths are in auto_path order - earliest has precedence for any particular pkg-version + + #REVIEW: what is to stop an auto_path package e.g from os, overriding a .tm ifneeded script from an item earlier in the package_mode list configured in punk's main.tcl? + #e.g when package_mode is {dev-os} we don't want a pkgIndex package from ::env(TCLLIBPATH) overriding a .tm from the dev paths (even if version nums the same) + #conversely we do want a dev path pkIndex package overriding an existing ifneeded script from a .tm in os + #to accomodate this - we may need to maintain a subdict in epoch of paths/path-prefixes to package_mode members os, dev, internal + + #this 'refresh' is really a 'reversion' to what was already stored in epoch pkg epochs added + # + + set e [dict get $epoch pkg current] + set pkgvdone [dict create] + set dict_added [dict get $epoch pkg epochs $e added] + #keys are in reverse order due to tclPkgUnknown processing order + set ordered_searchpaths [lreverse [dict keys $dict_added]];# ordered as in auto_path + + dict for {pkg versiond} $refresh_dict { + set versions [dict keys $versiond] + puts stderr "---->pkg:$pkg versions: $versions" + foreach searchpath $ordered_searchpaths { + set addedinfo [dict get $dict_added $searchpath] + set vidx -1 + foreach v $versions { + incr vidx + if {[dict exists $addedinfo $pkg $v]} { + ledit versions $vidx $vidx + set iscript [dict get $addedinfo $pkg $v scr] + #todo - find the iscript in the '$epoch pkg epochs added paths' lists and determine os vs dev vs internal + #(scanning for path directly in the ifneeded script for pkgs is potentially error prone) + #for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway) + set justaddedscript [package ifneeded $pkg $v] + if {$justaddedscript ne $iscript} { + puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions" + package ifneeded $pkg $v $iscript + #dict set pkgvdone $pkg $v 1 + } + } + } + if {[llength $versions] == 0} { + break + } + } } + + #puts "zipfs_tclPkgUnknown DONE" } + variable last_auto_path + variable last_tm_paths proc epoch_incr_pkg {args} { if {[catch { + variable last_auto_path global auto_path upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] + # ------------- + puts stderr "--> pkg epoch $prev_e -> $current_e" + puts stderr "args: $args" + puts stderr "last_auto: $last_auto_path" + puts stderr "auto_path: $auto_path" + # ------------- + if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { + #The auto_path changed, and is a pure addition of entry/entries + #commonly this is occurs where a single entry is added by a pkgIndex.Tcl + #e.g tcllib adds its base dir so that all pkgIndex.tcl files in subdirs are subsequently found + #consider autopath + #c:/libbase //zipfs:/app/libbase + #if both contain a tcllib folder with pkgIndex.tcl that extends auto_path, the auto_path extends as follows: + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase/tcllib + + #the tclPkgUnknown usedir loop (working from end of list towards beginning) will process these changes the first time dynamically + #as they occur: + #ie //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase c:/libbase/tcllib + + #A subsequent scan by tclPkgUnknown on the extended auto_path would process in the order: + #c:/libbase/tcllib c:/libbase //zipfs:/app/libbase/tcllib //zipfs:/app/libbase + + #re-order the new additions to come immediately following the longest common prefix entry + + set newitems [punk::libunknown::lib::ldiff $auto_path $last_auto_path] + + set update $last_auto_path + #no ledit or punk::lib::compat::ledit for 8.6 - so use linsert + foreach new $newitems { + set offset 0 + set has_prefix 0 + foreach ap [lreverse $update] { + if {[string match $ap* $new]} { + set has_prefix 1 + break + } + incr offset + } + if {$has_prefix} { + set update [linsert $update end-$offset $new] + } else { + lappend update $new + } + } + set auto_path $update + + + } + #else - if auto_path change wasn't just extra entries - leave as user specified + #review. + + set last_auto_path $auto_path + # ------------- dict set epoch pkg current $current_e dict set epoch pkg epochs $current_e [dict create] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } + if {[dict exists $epoch pkg epochs $prev_e indexes]} { - #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + #bring across each previous 'indexes' record *if* searchpath is within zipfs root static filesystem + # and searchpath is still a path below an auto_path entry. + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -710,6 +970,9 @@ tcl::namespace::eval punk::libunknown { } } } + + #---------------------------------------- + #store basic stats for previous epoch instead of all data. set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e indexes]] set index_count 0 dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { @@ -718,12 +981,28 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count $index_searchpath_count index_count $index_count] dict unset epoch pkg epochs $prev_e indexes + #---------------------------------------- } else { dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch pkg epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + if {"clearadded" in $args} { + dict set epoch pkg epochs $current_e added [dict create] + } else { + if {$has_zipfs} { + set zroot [zipfs root] + set prev_added [dict get $epoch pkg epochs $prev_e added] + set keep_added [dict filter $prev_added key $zroot*] + #bring across - each lib will have previous epoch number as the value indicating epoch in which it was found + #dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + dict set epoch pkg epochs $current_e added $keep_added + } else { + dict set epoch pkg epochs $current_e added [dict create] + } + } + + #store basic stats for previous epoch + #------------------------------------ set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e added] { @@ -735,37 +1014,31 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] dict unset epoch pkg epochs $prev_e added + #------------------------------------ } else { dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch pkg epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch pkg epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch pkg epochs $prev_e unfound - } } errM]} { - puts stderr "epoch_incr_pkg error\n $errM" + puts stderr "epoch_incr_pkg error\n $errM\n$::errorInfo" } } proc epoch_incr_tm {args} { if {[catch { upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch tm current] set current_e [expr {$prev_e + 1}] dict set epoch tm current $current_e dict set epoch tm epochs $current_e [dict create] set tmlist [tcl::tm::list] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } if {[dict exists $epoch tm epochs $prev_e indexes]} { #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch tm epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -795,8 +1068,21 @@ tcl::namespace::eval punk::libunknown { dict set epoch tm epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch tm epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + #todo? cycle through non-statics and add pkgs to pkg untracked if we are deleting 'added' records? + if {"clearadded" in $args} { + dict set epoch tm epochs $current_e added [dict create] + } else { + #bring across - each lib will have previous epoch number + #dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + if {$has_zipfs} { + set zroot [zipfs root] + dict set epoch tm epochs $current_e added [dict filter [dict get $epoch tm epochs $prev_e added] key $zroot*] + } else { + dict set epoch tm epochs $current_e added [dict create] + } + } + + set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e added] { @@ -811,26 +1097,77 @@ tcl::namespace::eval punk::libunknown { } else { dict set epoch tm epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch tm epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch tm epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch tm epochs $prev_e unfound - } } errM]} { puts stderr "epoch_incr_tm error\n $errM" } } - proc init {} { + #see what basic info we can gather *quickly* about the indexes for each version of a pkg that the package db knows about. + #we want no calls out to the actual filesystem - but we can use some 'file' calls such as 'file dirname', 'file split' (review -safe interp problem) + #in practice the info is only available for tm modules + proc packagedb_indexinfo {pkg} { + if {[string match ::* $pkg]} { + error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'" + } + set versions [lsort -command {package vcompare} [package versions $pkg]] + if {[llength $versions] == 0} { + set v [package provide $pkg] + } + set versionlist [list] + foreach v $versions { + set ifneededscript [package ifneeded $pkg $v] + if {[string trim $ifneededscript] eq ""} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + set scriptlines [split $ifneededscript \n] + if {[llength $scriptlines] > 1} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[catch {llength $ifneededscript}]} { + #scripts aren't necessarily 'list shaped' - we don't want to get into the weeds trying to make sense of arbitrary scripts. + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[lindex $ifneededscript 0] eq "package" && [lindex $ifneededscript 1] eq "provide" && [file extension [lindex $ifneededscript end]] eq ".tm"} { + set tmfile [lindex $ifneededscript end] + set nspath [namespace qualifiers $pkg] + if {$nspath eq ""} { + set base [file dirname $tmfile] + } else { + set nsparts [string map {:: " "} $nspath] ;#*naive* split - we are assuming (fairly reasonably) there are no namespaces containing spaces for a .tm module + set pathparts [file split [file dirname $tmfile]] + set baseparts [lrange $pathparts 0 end-[llength $nsparts]] + set base [file join {*}$baseparts] + } + lappend versionlist [list $v type tm index $tmfile indexbase $base script $ifneededscript] + } else { + #we could guess at the pkgindex.tcl file used based on simple pkg ifneeded scripts .tcl path compared to ::auto_index + #but without hitting filesystem to verify - it's unsatisfactory + lappend versionlist [list $v type unknown index "" indexbase "" script $ifneededscript] + } + } + return $versionlist + } + proc init {args} { + variable last_auto_path + set last_auto_path [set ::auto_path] + variable last_tm_paths + set last_tm_paths [set ::tcl::tm::paths] + + set callerposn [lsearch $args -caller] + if {$callerposn > -1} { + set caller [lindex $args $callerposn+1] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - caller:$caller\x1b\[m" + #puts stderr "punk::libunknown::init auto_path : $::auto_path" + #puts stderr "punk::libunknown::init tcl::tm::list: [tcl::tm::list]" + } + + + + if {[catch {tcl::tm::list} tmlist]} { set tmlist [list] } @@ -850,10 +1187,113 @@ tcl::namespace::eval punk::libunknown { #This is far from conclusive - there may be other renamers (e.g commandstack) return } + + + if {[info commands ::punk::libunknown::package] ne ""} { puts stderr "punk::libunknown::init already done - unnecessary call? info frame -1: [info frame -1]" return } + variable epoch + if {![info exists epoch]} { + set tmstate [dict create 0 {added {}}] + set pkgstate [dict create 0 {added {}}] + set tminfo [dict create current 0 epochs $tmstate untracked [dict create]] + set pkginfo [dict create current 0 epochs $pkgstate untracked [dict create]] + + set epoch [dict create scripts {} tm $tminfo pkg $pkginfo] + + #untracked: package names at time of punk::libunknown::init call - or passed with epoch when sharing epoch to another interp. + #The epoch state will need to be incremented and cleared of any 'added' records if any of these are requested during a package unknown call + #Because they were loaded prior to us tracking the epochs - and without trying to examine the ifneeded scripts we don't know the exact paths + #which were scanned to load them. Our 'added' key entries will not contain them because they weren't unknown + } else { + #we're accepting a pre-provided 'epoch' record (probably from another interp) + #the tm untracked and pkg untracked dicts indicate for which packages the pkg added, tm added etc data are not conclusive + #test + #todo? + } + #upon first libunknown::init in the interp, we need to add any of this interp's already known packages to the (possibly existing) tm untracked and pkg untracked dicts. + #(unless we can use packagedb_indexinfo to determine what was previously scanned?) + # review - what if the auto_path or tcl::tm::list was changed between initial scan and call of libunknown::init??? + # This is likely a common scenario?!!! + # For now this is a probable flaw in the logic - we need to ensure libunknown::init is done first thing + # or suffer additional scans.. or document ?? + #ideally init should be called in each interp before any scans for packages so that the list of untracked is minimized. + set pkgnames [package names] + foreach p $pkgnames { + if {[string tolower $p] in {punk::libunknown tcl::zlib tcloo tcl::oo tcl}} { + continue + } + set versions [package versions $p] + if {[llength $versions] == 0} { + continue + } + set versionlist [packagedb_indexinfo $p] + if {[llength $versionlist] == 0} { + continue + } else { + foreach vdata $versionlist { + #dict set epoch scripts $p [lindex $vdata 0] [package ifneeded $p [lindex $vdata 0]] + dict set epoch scripts $p [lindex $vdata 0] [lindex $vdata 8]] + } + if {[lsearch -index 6 $versionlist ""] > -1} { + #There exists at least one empty indexbase for this package - we have to treat it as untracked + dict set epoch tm untracked $p "" ;#value unimportant + dict set epoch pkg untracked $p "" ;#value unimportant + } else { + #update the epoch info with where the tm versions came from + #(not tracking version numbers in epoch - just package to the indexbase) + foreach vdata $versionlist { + lassign $vdata v _t type _index index _indexbase indexbase _script iscript + if {$type eq "tm"} { + if {![dict exists $epoch tm epochs 0 added $indexbase]} { + #dict set epoch tm epochs 0 added $indexbase [dict create $p [dict create e 0 v $v]] + dict set epoch tm epochs 0 added $indexbase $p $v [dict create e 0 scr $iscript] + } else { + set idxadded [dict get $epoch tm epochs 0 added $indexbase] + #dict set idxadded $p [dict create e 0 v $v] + dict set idxadded $p $v [dict create e 0 scr $iscript] + dict set epoch tm epochs 0 added $indexbase $idxadded + } + dict unset epoch tm untracked $p + } elseif {$type eq "pkg"} { + #todo? tcl doesn't give us good introspection on package indexes for packages + #dict unset epoch pkg untracked $p + } + } + } + } + } + + + + + #------------------------------------------------------------- + #set all_untracked [dict keys [dict get $epoch untracked]] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - pkg all_untracked:\x1b\[m [dict size [dict get $epoch pkg untracked]]" + #if {[dict exists $epoch pkg untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in pkg untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in pkg untracked \x1b\[m " + #} + #puts stderr "\x1b\[1\;33m punk::libunknown::init - tm all_untracked:\x1b\[m [dict size [dict get $epoch tm untracked]]" + #if {[dict exists $epoch tm untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in tm untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in tm untracked \x1b\[m " + #} + #------------------------------------------------------------- trace add variable ::auto_path write ::punk::libunknown::epoch_incr_pkg trace add variable ::tcl::tm::paths write ::punk::libunknown::epoch_incr_tm @@ -870,6 +1310,7 @@ tcl::namespace::eval punk::libunknown { #forgetting Tcl or tcl seems to be a bad idea - package require doesn't work afterwards (independent of this pkg) set forgets_requested [lrange $args 1 end] set ok_forgets [list] + upvar ::punk::libunknown::epoch epoch foreach p $forgets_requested { #'package files' not avail in early 8.6 #There can be other custom 'package ifneeded' scripts that don't use source - but still need to be forgotten. @@ -880,7 +1321,7 @@ tcl::namespace::eval punk::libunknown { # lappend ok_forgets $p #} #What then? Hardcoded only for now? - if {$p ni {tcl Tcl tcl::oo}} { + if {$p ni {tcl Tcl tcl::oo tk}} { #tcl::oo returns a comment only for its package provide script "# Already present, OK?" # - so we can't use empty 'ifneeded' script as a determinant. set vpresent [package provide $p] @@ -890,11 +1331,13 @@ tcl::namespace::eval punk::libunknown { set ifneededscript [package ifneeded $p $vpresent] if {[string trim $ifneededscript] ne ""} { lappend ok_forgets $p + dict unset epoch scripts $p } } else { #not loaded - but may have registered ifneeded script(s) in the package database #assume ok to forget lappend ok_forgets $p + dict unset epoch scripts $p } } } @@ -1021,7 +1464,9 @@ tcl::namespace::eval punk::libunknown { #} if {![interp issafe]} { + #J2 package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown} + #package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::tclPkgUnknown} } } @@ -1030,11 +1475,280 @@ tcl::namespace::eval punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + proc package_query {pkgname} { + variable epoch + + if {[dict exists $epoch tm untracked $pkgname]} { + set pkg_info "$pkgname tm UNTRACKED" + } else { + set pkg_info "$pkgname not in tm untracked" + } + if {[dict exists $epoch pkg untracked $pkgname]} { + append pkg_info \n "$pkgname pkg UNTRACKED" + } else { + append pkg_info \n "$pkgname not in pkg untracked" + } + + set pkg_epoch [dict get $epoch pkg current] + #set epoch_info [dict get $epoch pkg epochs $pkg_epoch] + #pkg entries are processed by package unknown in reverse - so their order of creaation is opposite to ::auto_path + set r_added [dict create] + foreach path [lreverse [dict keys [dict get $epoch pkg epochs $pkg_epoch added]]] { + dict set r_added $path [dict get $epoch pkg epochs $pkg_epoch added $path] + } + + #set pkg_added [punk::lib::showdict $r_added */$pkgname] + #set added [textblock::frame -title $title $pkg_added] + set rows [list] + dict for {path pkgs} $r_added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + set scriptlen [string length [dict get $data scr]] + append c3 "$v epoch[dict get $data e] ifneededchars:$scriptlen" \n + } + } + set r [list $path $c2 $c3] + lappend rows $r + } + set title "[punk::ansi::a+ green] PKG epoch $pkg_epoch - added [punk::ansi::a]" + set added [textblock::table -title $title -headers [list Path Pkgcount $pkgname] -rows $rows] + + + set pkg_row $added + + set tm_epoch [dict get $epoch tm current] + #set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + set added [dict get $epoch tm epochs $tm_epoch added] + set rows [list] + dict for {path pkgs} $added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + append c3 "$v $data" \n + } + } + set r [list $c1 $c2 $c3] + lappend rows $r + } + set title "TM epoch $tm_epoch - added" + #set added [textblock::frame -title $title $tm_added] + set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows] + + set tm_row $added + + return $pkg_info\n$pkg_row\n$tm_row + } + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::libunknown ---}] } # == === === === === === === === === === === === === === === +namespace eval punk::libunknown { + #for 8.6 compat + if {"::ledit" ni [info commands ::ledit]} { + #maint: taken from punk::lib + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } + + #maint: taken from punk::lib + proc lindex_resolve {len index} { + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + if {[string is integer -strict $index]} { + #can match +i -i + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } else { + #integer may still have + sign - normalize with expr + return [expr {$index}] + } + } else { + if {[string match end* $index]} { + if {$index ne "end"} { + set op [string index $index 3] + set offset [string range $index 4 end] + if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} + if {$op eq "+" && $offset != 0} { + return -2 + } + } else { + #index is 'end' + set index [expr {$len-1}] + if {$index < 0} { + #special case - 'end' with empty list - treat end like a positive number out of bounds + return -2 + } else { + return $index + } + } + if {$offset == 0} { + set index [expr {$len-1}] + if {$index < 0} { + return -2 ;#special case as above + } else { + return $index + } + } else { + #by now, if op = + then offset = 0 so we only need to handle the minus case + set index [expr {($len-1) - $offset}] + } + if {$index < 0} { + return -3 + } else { + return $index + } + } else { + #plain +- already handled above. + #we are trying to avoid evaluating unbraced expr of potentially insecure origin + if {[regexp {(.*)([+-])(.*)} $index _match a op b]} { + if {[string is integer -strict $a] && [string is integer -strict $b]} { + if {$op eq "-"} { + set index [expr {$a - $b}] + } else { + set index [expr {$a + $b}] + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } + return $index + } + } + } + } +} + +tcl::namespace::eval punk::libunknown::lib { + + #A version of textutil::string::longestCommonPrefixList + #(also as ::punk::lib::longestCommonPrefixList) + proc longestCommonPrefix {items} { + if {[llength $items] <= 1} { + return [lindex $items 0] + } + set items [lsort $items[unset items]] + set min [lindex $items 0] + set max [lindex $items end] + #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) + #(sort order nothing to do with length - e.g min may be longer than max) + if {[string length $min] > [string length $max]} { + set temp $min + set min $max + set max $temp + } + set n [string length $min] + set prefix "" + set i -1 + while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { + append prefix $c + } + return $prefix + } + #maint: from punk::lib::ldiff + proc ldiff {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + set result [list] + foreach item $fromlist { + if {$item ni $removeitems} { + lappend result $item + } + } + return $result + } + proc intersect2 {A B} { + #taken from tcl version of struct::set::Intersect + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + + # This is slower than local vars, but more robust + if {[llength $B] > [llength $A]} { + ::set res $A + ::set A $B + ::set B $res + } + ::set res {} + foreach x $A { + ::set ($x) {} + } + foreach x $B { + if {[info exists ($x)]} { + lappend res $x + } + } + return $res + } + proc is_list_all_in_list {A B} { + if {[llength $A] > [llength $B]} {return 0} + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } +} # ----------------------------------------------------------------------------- # register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked diff --git a/src/bootsupport/modules/punk/mix-0.2.tm b/src/bootsupport/modules/punk/mix-0.2.tm index 24ef156c..1ac6a836 100644 --- a/src/bootsupport/modules/punk/mix-0.2.tm +++ b/src/bootsupport/modules/punk/mix-0.2.tm @@ -9,12 +9,12 @@ tcl::namespace::eval punk::mix { package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap set t [time { - if {[catch {punk::mix::templates::provider register *} errM]} { - puts stderr "punk::mix failure during punk::mix::templates::provider register *" - puts stderr $errM - puts stderr "-----" - puts stderr $::errorInfo - } + if {[catch {punk::mix::templates::provider register *} errM]} { + puts stderr "punk::mix failure during punk::mix::templates::provider register *" + puts stderr $errM + puts stderr "-----" + puts stderr $::errorInfo + } }] puts stderr "->punk::mix::templates::provider register * t=$t" } diff --git a/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm index 5e12b9a2..3fb1e001 100644 --- a/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm @@ -53,11 +53,6 @@ namespace eval punk::mix::commandset::loadedlib { #REVIEW - this doesn't result in full scans catch {package require frobznodule666} ;#ensure pkg system has loaded/searched for everything - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } set packages [package names] set matches [list] foreach search $searchstrings { @@ -85,11 +80,7 @@ namespace eval punk::mix::commandset::loadedlib { # set versions $v #} } - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {$opt_highlight} { set v [package provide $m] if {$v ne ""} { @@ -188,11 +179,6 @@ namespace eval punk::mix::commandset::loadedlib { } proc info {libname} { - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } catch {package require $libname 1-0} ;#ensure pkg system has loaded/searched - using unsatisfiable version range set pkgsknown [package names] if {[set posn [lsearch $pkgsknown $libname]] >= 0} { @@ -201,11 +187,7 @@ namespace eval punk::mix::commandset::loadedlib { puts stderr "Package not found as available library/module - check tcl::tm::list and \$auto_path" } set versions [package versions [lindex $libname 0]] - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {![llength $versions]} { puts stderr "No version numbers found for library/module $libname" return false diff --git a/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 2bc0f01c..723ce06e 100644 --- a/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -77,6 +77,12 @@ namespace eval punk::mix::commandset::module { return $result } #require current dir when calling to be the projectdir, or + punk::args::define { + @dynamic + @id -id "::punk::mix::commandset::module::templates" + @cmd -name "punk::mix::commandset::module::templates" + ${[punk::args::resolved_def -antiglobs {@id @cmd} "::punk::mix::commandset::module::templates_dict"]} + } proc templates {args} { set tdict_low_to_high [templates_dict {*}$args] #convert to screen order - with higher priority at the top @@ -135,16 +141,17 @@ namespace eval punk::mix::commandset::module { globsearches -default * -multiple 1 } proc templates_dict {args} { - set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + #set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + set argd [punk::args::parse $args withid ::punk::mix::commandset::module::templates_dict] package require punk::cap if {[punk::cap::capability_has_handler punk.templates]} { set template_folder_dict [punk::cap::call_handler punk.templates get_itemdict_moduletemplates {*}$args] } else { put stderr "get_template_basefolders WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } - } + } + - set moduletypes [punk::mix::cli::lib::module_types] punk::args::define [subst { @id -id ::punk::mix::commandset::module::new @@ -178,7 +185,7 @@ namespace eval punk::mix::commandset::module { set argd [punk::args::get_by_id ::punk::mix::commandset::module::new $args] lassign [dict values $argd] leaders opts values received set module [dict get $values module] - + #set opts [dict merge $defaults $args] #todo - review compatibility between -template and -type diff --git a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index f670c8c0..8abe694e 100644 --- a/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -592,10 +592,23 @@ namespace eval punk::mix::commandset::project { namespace export * namespace path [namespace parent] + punk::args::define { + @id -id ::punk::mix::commandset::project::collection::_default + @cmd -name "punk::mix::commandset::project::collection::_default"\ + -summary\ + "List projects under fossil managment."\ + -help\ + "List projects under fossil management, showing fossil db location and number of checkouts" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * + } #e.g imported as 'projects' - proc _default {{glob {}} args} { + proc _default {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::collection::_default] + set globlist [dict get $argd values glob] + #*** !doctools - #[call [fun _default] [arg glob] [opt {option value...}]] + #[call [fun _default] [arg glob...]] #[para]List projects under fossil management, showing fossil db location and number of checkouts #[para]The glob argument is optional unless option/value pairs are also supplied, in which case * should be explicitly supplied #[para]glob restricts output based on the name of the fossil db file e.g s* for all projects beginning with s @@ -604,7 +617,7 @@ namespace eval punk::mix::commandset::project { #[para] punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection #[para]Will result in the command being available as projects package require overtype - set db_projects [lib::get_projects $glob] + set db_projects [lib::get_projects {*}$globlist] set col1items [lsearch -all -inline -index 0 -subindices $db_projects *] set col2items [lsearch -all -inline -index 1 -subindices $db_projects *] set checkouts [lsearch -all -inline -index 2 -subindices $db_projects *] @@ -1012,12 +1025,21 @@ namespace eval punk::mix::commandset::project { #consider using punk::cap to enable multiple template-substitution providers with their own set of tagnames and/or tag wrappers, where substitution providers are all run return [string cat % $tagname %] } - #get project info only by opening the central confg-db - #(will not have proper project-name etc) - proc get_projects {{globlist {}} args} { - if {![llength $globlist]} { - set globlist [list *] - } + punk::args::define { + @id -id ::punk::mix::commandset::project::lib::get_projects + @cmd -name punk::mix::commandset::project::lib::get_projects\ + -summary\ + "List projects referred to by central fossil config-db."\ + -help\ + "Get project info only by opening the central fossil config-db + (will not have proper project-name etc)" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * -optional 1 + } + proc get_projects {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::lib::get_projects] + set globlist [dict get $argd values glob] + set fossil_prog [auto_execok fossil] set configdb [punk::repo::fossil_get_configdb] diff --git a/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index e2f44ad3..b40be865 100644 --- a/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -940,7 +940,8 @@ tcl::namespace::eval punk::nav::fs { #windows doesn't consider dotfiles as hidden - mac does (?) #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden if {$::tcl_platform(platform) ne "windows"} { - lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + #lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + lappend flaggedhidden {*}[tcl::prefix::all [list {*}$dirs {*}$files] .] #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely #set flaggedhidden [lsort -unique $flaggedhidden] diff --git a/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/bootsupport/modules/punk/ns-0.1.0.tm index 0f609b4f..6bd826e2 100644 --- a/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -177,10 +177,10 @@ tcl::namespace::eval punk::ns { } else { set fq_nspath $nspath } - if {[catch {nseval_ifexists $fq_nspath {}}]} { - return 0 - } else { + if {[nseval_ifexists $fq_nspath {::string cat ok}] eq "ok"} { return 1 + } else { + return 0 } } @@ -408,6 +408,7 @@ tcl::namespace::eval punk::ns { proc nstail {nspath args} { #normalize the common case of :::: set nspath [string map {:::: ::} $nspath] + #it's unusual - but namespaces *can* have spaced in them. set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] @@ -757,13 +758,20 @@ tcl::namespace::eval punk::ns { } set marks [dict create oo \u25c6 ooc \u25c7 ooo \u25c8 punkargs \U1f6c8 ensemble \u24ba native \u24c3 unknown \U2370] if {[llength $ansinames]} { - return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type][punk::ansi::a]" + return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type]\x1b\[0m" } else { return [dict get $marks $type] } } #REVIEW - ansi codes can be *very* confusing to the user when trying to handle lists etc.. + punk::args::define { + @id -id ::punk::ns::get_nslist + @cmd -name punk::ns::get_nslist + @opts + -match -default "" + -nsdict -type dict -default {} + } proc get_nslist {args} { set known_types [list children commands exported imported aliases procs ensembles ooclasses ooobjects ooprivateobjects ooprivateclasses native coroutines interps zlibstreams] set defaults [dict create\ @@ -774,6 +782,9 @@ tcl::namespace::eval punk::ns { set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- set fq_glob [dict get $opts -match] + if {$fq_glob eq ""} { + set fq_glob [uplevel 1 nsthis]::* + } set requested_types [dict get $opts -types] set opt_nsdict [dict get $opts -nsdict] @@ -834,7 +845,7 @@ tcl::namespace::eval punk::ns { set zlibstreams [list] set usageinfo [list] - if {$opt_nsdict eq ""} { + if {![dict size $opt_nsdict]} { set nsmatches [get_ns_dicts $fq_glob -allbelow 0] set itemcount 0 set matches_with_results [list] @@ -866,6 +877,8 @@ tcl::namespace::eval punk::ns { } if {"commands" in $types} { set commands [dict get $contents commands] + } + set usageinfo [dict get $contents usageinfo] foreach t $types { switch -- $t { exported { @@ -909,8 +922,6 @@ tcl::namespace::eval punk::ns { } } } - set usageinfo [dict get $contents usageinfo] - } set numchildren [llength $children] if {$numchildren} { @@ -1067,7 +1078,7 @@ tcl::namespace::eval punk::ns { } else { } if {$cmd in $imported} { - set prefix [overtype::right $prefix "-[a+ yellow bold]I[a+]"] + set prefix [overtype::right $prefix "-[a+ yellow bold]I[a]"] } } if {$cmd in $usageinfo} { @@ -1075,7 +1086,8 @@ tcl::namespace::eval punk::ns { } else { set u "" } - set cmd$i "${prefix} $c$cmd_display$u" + #set cmd$i "${prefix} $c$cmd_display$u" + set cmd$i "${prefix} [punk::ansi::ansiwrap -rawansi $c $cmd_display]$u" #set c$i $c set c$i "" lappend seencmds $cmd @@ -1146,7 +1158,11 @@ tcl::namespace::eval punk::ns { the child namespaces and commands within the namespace(s) matched by glob." @opts - -nspathcommands -type boolean -default 0 + -nspathcommands -type boolean -default 0 -help\ + "When a namespace has entries configured in 'namespace path', the default result for nslist + will display just a basic note: 'Also resolving cmds in namespace paths: '. + If -nspathcommands is true, it will also display subtables showing the commands resolvable + via any such listed namespaces." -types @values -min 0 -max -1 glob -multiple 1 -optional 1 -default "*" @@ -1205,9 +1221,9 @@ tcl::namespace::eval punk::ns { if {[dict size [dict get $nsdict namespacepath]]} { set path_text "" if {!$opt_nspathcommands} { - append path_text \n " also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" + append path_text \n " Also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" } else { - append path_text \n " also resolving cmds in namespace paths:" + append path_text \n " Also resolving cmds in namespace paths:" set nspathdict [dict get $nsdict namespacepath] if {!$has_textblock} { dict for {k v} $nspathdict { @@ -1216,8 +1232,14 @@ tcl::namespace::eval punk::ns { append path_text \n " cmds: $cmds" } } else { + #todo - change to display in column order to be same as main command listing dict for {k v} $nspathdict { - set t [textblock::list_as_table -title $k -columns 6 [lsort [dict get $v commands]]] + set pathcommands [dict get $v commands] + set columns 6 + if {[llength $pathcommands] < 6} { + set columns [llength $v] + } + set t [textblock::list_as_table -title $k -columns $columns [lsort $pathcommands]] append path_text \n $t } } @@ -1423,7 +1445,7 @@ tcl::namespace::eval punk::ns { } } return $matches - }] + }]] } else { lappend matched {*}[tcl::namespace::eval $location [list ::info commands [nsjoin ${location} $p]]] @@ -2397,14 +2419,16 @@ tcl::namespace::eval punk::ns { if {$is_ensembleparam} { #review lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } if {![llength $allchoices]} { #review - only leaders with a defined set of choices are eligible for consideration as a subcommand lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } @@ -2420,7 +2444,8 @@ tcl::namespace::eval punk::ns { } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args set resolvelist [list {*}$specid {*}$nextqueryargs {*}$queryargs_untested] @@ -2510,8 +2535,12 @@ tcl::namespace::eval punk::ns { punk::args::define { @id -id ::punk::ns::forms - @cmd -name punk::ns::forms -help\ - "Return names for each form of a command" + @cmd -name punk::ns::forms\ + -summary\ + "List command forms."\ + -help\ + "Return names for each form of a command. + Most commands are single-form and will only return the name '_default'." @opts @values -min 1 -max -1 cmditem -multiple 1 -optional 0 @@ -2523,12 +2552,37 @@ tcl::namespace::eval punk::ns { set id [dict get $cmdinfo origin] ::punk::args::forms $id } + + + punk::args::define { + @id -id ::punk::ns::eg + @cmd -name punk::ns::eg\ + -summary\ + "Return command examples."\ + -help\ + "Return the -help info from the @examples directive + in a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + } + proc eg {args} { + set argd [::punk::args::parse $args withid ::punk::ns::eg] + set cmdmembers [dict get $argd values cmditem] + set cmdinfo [uplevel 1 [list ::punk::ns::resolve_command {*}$cmdmembers]] ;#resolve from calling context + set resolved_id [dict get $cmdinfo origin] + set result [::punk::args::eg $resolved_id] + } + + punk::args::define { @id -id ::punk::ns::synopsis - @cmd -name punk::ns::synopsis -help\ + @cmd -name punk::ns::synopsis\ + -summary\ + "Return command synopsis."\ + -help\ "Return synopsis for each form of a command on separate lines. - If -form is given, supply only + If -form formname| is given, supply only the synopsis for that form. " @opts @@ -2564,8 +2618,12 @@ tcl::namespace::eval punk::ns { full - summary { set resultstr "" foreach synline [split $syn \n] { - #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n - append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + if {[string range $synline 0 1] eq "# "} { + append resultstr $synline \n + } else { + #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n + append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + } } set resultstr [string trimright $resultstr \n] #set resultstr [join [lreplace $syn 0 0 {*}$idparts] " "] @@ -2591,7 +2649,10 @@ tcl::namespace::eval punk::ns { punk::args::define { @dynamic @id -id ::punk::ns::arginfo - @cmd -name punk::ns::arginfo -help\ + @cmd -name punk::ns::arginfo\ + -summary\ + "Command usage/help."\ + -help\ "Show usage info for a command. It supports the following: 1) Procedures or builtins for which a punk::args definition has @@ -2618,6 +2679,9 @@ tcl::namespace::eval punk::ns { } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { -form -default 0 -help\ "Ordinal index or name of command form" + -grepstr -default "" -type list -typesynopsis regex -help\ + "list consisting of regex, optionally followed by ANSI names for highlighting + (incomplete - todo)" -- -type none -help\ "End of options marker Use this if the command to view begins with a -" @@ -2642,6 +2706,8 @@ tcl::namespace::eval punk::ns { set querycommand [dict get $values commandpath] set queryargs [dict get $values subcommand] + set grepstr [dict get $opts -grepstr] + set opts [dict remove $opts -grepstr] #puts stdout "---------------------arginfo: '$args' querycommand:'$querycommand' queryargs:'$queryargs'" #todo - similar to corp? review corp resolution process @@ -2905,7 +2971,8 @@ tcl::namespace::eval punk::ns { break } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args #set numvals [expr {[llength $queryargs]+1}] @@ -3020,8 +3087,11 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} new" - @cmd -name "${$origin} new" -help\ - "create object with specified command name. + @cmd -name "${$origin} new"\ + -summary\ + "Create new object instance."\ + -help\ + "create object with autogenerated command name. Arguments are passed to the constructor." @values }] @@ -3071,7 +3141,10 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} create" - @cmd -name "${$origin} create" -help\ + @cmd -name "${$origin} create"\ + -summary\ + "Create new object instance with specified command name."\ + -help\ "create object with specified command name. Arguments following objectName are passed to the constructor." @values -min 1 @@ -3124,7 +3197,10 @@ tcl::namespace::eval punk::ns { # but we may want notes about a specific destructor set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} destroy" - @cmd -name "destroy" -help\ + @cmd -name "destroy"\ + -summary\ + "delete object instance."\ + -help\ "delete object, calling destructor if any. destroy accepts no arguments." @values -min 0 -max 0 @@ -3601,6 +3677,13 @@ tcl::namespace::eval punk::ns { set msg "Undocumented command $origin. Type: $cmdtype" } } + if {[llength $grepstr] != 0} { + if {[llength $grepstr] == 1} { + return [punk::grepstr -no-linenumbers -highlight red [lindex $grepstr 0] $msg] + } else { + return [punk::grepstr -no-linenumbers -highlight [lrange $grepstr 1 end] [lindex $grepstr 0] $msg] + } + } return $msg } @@ -3620,6 +3703,21 @@ tcl::namespace::eval punk::ns { comment inserted to display information such as the namespace origin. Such a comment begins with #corp#." @opts + -syntax -default basic -choices {none basic}\ + -choicelabels { + none\ + " Plain text output" + basic\ + " Comment and bracket highlights. + This is a basic colourizer - not + a full Tcl syntax highlighter." + }\ + -help\ + "Type of syntax highlighting on result. + Note that -syntax none will always return a proper Tcl + List: proc + - but a syntax highlighter may return a string that + is not a Tcl list." @values -min 1 -max -1 commandname -help\ "May be either the fully qualified path for the command, @@ -3628,7 +3726,8 @@ tcl::namespace::eval punk::ns { } proc corp {args} { set argd [punk::args::parse $args withid ::punk::ns::corp] - set path [dict get $argd values commandname] + set path [dict get $argd values commandname] + set syntax [dict get $argd opts -syntax] #thanks to Richard Suchenwirth for the original - wiki.tcl-lang.org/page/corp #Note: modified here to support aliases and relative/absolute name (with respect to namespace .ie ::name vs name) if {[info exists punk::console::tabwidth]} { @@ -3713,7 +3812,19 @@ tcl::namespace::eval punk::ns { lappend argl $a } #list proc [nsjoin ${targetns} $name] $argl $body - list proc $resolved $argl $body + switch -- $syntax { + basic { + #rudimentary colourising only + set argl [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $argl] + set body [punk::grepstr -return all -highlight green {^\s*#.*} $body] ;#Note, will not highlight comments at end of line - like this one. + set body [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $body] + set body [punk::grepstr -return all -highlight tk-orange {\[|\]} $body] + #ansi colourised items in list format may not always have desired string representation (list escaping can occur) + #return as a string - which may not be a proper Tcl list! + return "proc $resolved {$argl} {\n$body\n}" + } + } + list proc $resolved $argl $body } @@ -3799,13 +3910,53 @@ tcl::namespace::eval punk::ns { } + punk::args::define { + @id -id ::punk::ns::pkguse + @cmd -name punk::ns::pkguse -help\ + "Load package and move to namespace of the same name if run + interactively with only pkg/namespace argument. + if script and args are supplied, the + script runs in the namespace with the args passed to the script. + + todo - further documentation" + @leaders -min 1 -max 1 + pkg_or_existing_ns -type string + @opts + -vars -type none -help\ + "whether to capture namespace vars for use in the supplied script" + -nowarnings -type none + @values -min 0 -max -1 + script -type string -optional 1 + arg -type any -optional 1 -multiple 1 + } #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically - proc pkguse {pkg_or_existing_ns args} { - lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs - set use_vars [expr {"-vars" in $runopts}] - set no_warnings [expr {"-nowarnings" in $runopts}] + proc pkguse {args} { + set argd [punk::args::parse $args withid ::punk::ns::pkguse] + lassign [dict values $argd] leaders opts values received + puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + + set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] + if {[dict exists $received script]} { + set scriptblock [dict get $values script] + } else { + set scriptblock "" + } + if {[dict exists $received arg]} { + set arglist [dict get $values arg] + } else { + set arglist [list] + } + + set use_vars [dict exists $received "-vars"] + set no_warnings [dict exists $received "-nowarnings"] + + #lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs + #set use_vars [expr {"-vars" in $runopts}] + #set no_warnings [expr {"-nowarnings" in $runopts}] + + set ver "" @@ -3883,7 +4034,7 @@ tcl::namespace::eval punk::ns { } } if {[tcl::namespace::exists $ns]} { - if {[llength $cmdargs]} { + if {[dict exists $received script]} { set binding {} #if {[info level] == 1} { # #up 1 is global @@ -3923,7 +4074,7 @@ tcl::namespace::eval punk::ns { } ] - set arglist [lassign $cmdargs scriptblock] + #set arglist [lassign $cmdargs scriptblock] if {[string first "\n" $scriptblock] <0 && [string first {$args} $scriptblock] <0} { #one liner without use of $args append scriptblock { {*}$args} diff --git a/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index 317fc9de..dabf7f8e 100644 --- a/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,9 +110,9 @@ tcl::namespace::eval punk::packagepreference { #[list_begin definitions] lappend PUNKARGS [list { - @id -id ::punk::packagepreference::install - @cmd -name ::punk::packagepreference::install -help\ - "Install override for ::package builtin - for 'require' subcommand only." + @id -id ::punk::packagepreference::uninstall + @cmd -name ::punk::packagepreference::uninstall -help\ + "Uninstall override for ::package builtin - for 'require' subcommand only." @values -min 0 -max 0 }] proc uninstall {} { @@ -194,7 +194,7 @@ tcl::namespace::eval punk::packagepreference { if {!$is_exact && [llength $vwant] <= 1 } { #required version unspecified - or specified singularly set available_versions [$COMMANDSTACKNEXT_ORIGINAL versions $pkg] - if {[llength $available_versions] > 1} { + if {[llength $available_versions] >= 1} { # --------------------------------------------------------------- #An attempt to detect dll/so loaded and try to load same version #dll/so files are often named with version numbers that don't contain dots or a version number at all @@ -202,9 +202,11 @@ tcl::namespace::eval punk::packagepreference { set pkgloadedinfo [lsearch -nocase -inline -index 1 [info loaded] $pkg] if {[llength $pkgloadedinfo]} { - puts stderr "--> pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and multiple versions available" - lassign $pkgloadedinfo path name - set lcpath [string tolower $path] + if {[llength $available_versions] > 1} { + puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + } + lassign $pkgloadedinfo loaded_path name + set lc_loadedpath [string tolower $loaded_path] #first attempt to find a match for our loaded sharedlib path in a *simple* package ifneeded statement. set lcpath_to_version [dict create] foreach av $available_versions { @@ -212,17 +214,19 @@ tcl::namespace::eval punk::packagepreference { #ifneeded script not always a valid tcl list if {![catch {llength $scr} scrlen]} { if {$scrlen == 3 && [lindex $scr 0] eq "load" && [string match -nocase [lindex $scr 2] $pkg]} { + #a basic 'load ' statement dict set lcpath_to_version [string tolower [lindex $scr 1]] $av } } } - if {[dict exists $lcpath_to_version $lcpath]} { - set lversion [dict get $lcpath_to_version $lcpath] + if {[dict exists $lcpath_to_version $lc_loadedpath]} { + set lversion [dict get $lcpath_to_version $lc_loadedpath] } else { #fallback to a best effort guess based on the path - set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $path $pkg] + set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $loaded_path $pkg] } + #puts "====lcpath_to_version: $lcpath_to_version" if {$lversion ne ""} { #name matches pkg #hack for known dll version mismatch @@ -232,24 +236,103 @@ tcl::namespace::eval punk::packagepreference { if {[llength $vwant] == 1} { #todo - still check vsatisfies - report a conflict? review } - return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + #return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + try { + set result [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + } trap {} {emsg eopts} { + #REVIEW - this occurred in punkmagic (rebuild of tclmagic) - probably due to multiple versions of registry + #under different auto_path folders - and mal-ordering in punk::libunknown's tclPkgUnknown + #May be obsolete.. issue still not clear + + #A hack for 'couldn't open "": permission denied' + #This happens for example with the tcl9registry13.dll when loading from zipfs - but not in all systems, and not for all dlls. + #exact cause unknown. + #e.g + #%package ifneeded registry 1.3.7 + #- load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #%load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #couldn't open "C:/Users/sleek/AppData/Local/Temp/TCL00003cf8/tcl9registry13.dll": permission denied + + #a subsequent load of the path used in the error message works. + + #if {[string match "couldn't open \"*\": permission denied" $emsg]} {} + if {[regexp {couldn't open "(.*)":.*permission denied.*} $emsg _ newpath]} { + #Since this is a hack that shouldn't be required - be noisy about it. + puts stderr ">>> $emsg" + puts stderr "punk::packagepreference::require hack: Re-trying load of $pkg with path: $newpath" + return [load $newpath $pkg] + } else { + #puts stderr "??? $emsg" + #dunno - re-raise + return -options $eopts $emsg + } + } + return $result } + #else puts stderr "> no version determined for pkg: $pkg loaded_path: $loaded_path" } } } # --------------------------------------------------------------- - set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + #?? + #set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + + if {[regexp {[A-Z]} $pkg]} { + #legacy package names #only apply catch & retry if there was a cap - otherwise we'll double try for errors unrelated to capitalisation if {[catch {$COMMANDSTACKNEXT require [string tolower $pkg] {*}$vwant} v]} { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } } else { - return $v + set require_result $v } } else { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + #return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } + } + #--------------------------------------------------------------- + #load relevant punk::args:: package(s) + #todo - review whether 'packagepreference' is the right place for this. + #It is conceptually different from the main functions of packagepreference, + #but we don't really want to have a chain of 'package' overrides slowing performance. + #there may be a more generic way to add soft side-dependencies that the original package doesn't/can't specify. + #--------------------------------------------------------------- + + set lc_pkg [string tolower $pkg] + #todo - lookup list of docpkgs for a package? from where? + #we should have the option to not load punk::args:: at all for many(most?) cases where they're unneeded. + #e.g skip if not ::tcl_interactive? + switch -exact -- $lc_pkg { + tcl { + set docpkgs [list tclcore] + } + tk { + set docpkgs [list tkcore] + } + default { + set docpkgs [list $lc_pkg] + } + } + foreach dp $docpkgs { + #review - versions? + #we should be able to load more specific punk::args pkg based on result of [package present $pkg] + catch { + #$COMMANDSTACKNEXT require $pkg {*}$vwant + #j2 + $COMMANDSTACKNEXT require punk::args::$dp + } } + #--------------------------------------------------------------- + return $require_result } default { return [$COMMANDSTACKNEXT {*}$args] diff --git a/src/bootsupport/modules/punk/path-0.1.0.tm b/src/bootsupport/modules/punk/path-0.1.0.tm index ff48fcb0..54ee4080 100644 --- a/src/bootsupport/modules/punk/path-0.1.0.tm +++ b/src/bootsupport/modules/punk/path-0.1.0.tm @@ -325,7 +325,8 @@ namespace eval punk::path { lappend finalparts .. } default { - lpop finalparts + #lpop finalparts + ledit finalparts end end } } } @@ -345,7 +346,8 @@ namespace eval punk::path { switch -exact -- $p { . - "" {} .. { - lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + #lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + ledit finalparts end end ;#uses punk::lib::compat::ledit if on < 8.7 } default { lappend finalparts $p diff --git a/src/bootsupport/modules/punk/pipe-1.0.tm b/src/bootsupport/modules/punk/pipe-1.0.tm index 0b5501ac..2b0500b8 100644 --- a/src/bootsupport/modules/punk/pipe-1.0.tm +++ b/src/bootsupport/modules/punk/pipe-1.0.tm @@ -373,6 +373,7 @@ tcl::namespace::eval punk::pipe::lib { if {$end_var_posn > 0} { #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. #lassign [scan $token %${end_var_posn}s%s] var spec + #lassign [punk::lib::string_splitbefore $token $end_var_posn] var spec set var [string range $token 0 $end_var_posn-1] set spec [string range $token $end_var_posn end] ;#key section includes the terminal char which ended the var and starts the spec } else { @@ -430,7 +431,7 @@ tcl::namespace::eval punk::pipe::lib { } #if {[string length $token]} { - # #lappend varlist [splitstrposn $token $end_var_posn] + # #lappend varlist [punk::lib::string_splitbefore $token $end_var_posn] # set var $token # set spec "" # if {$end_var_posn > 0} { diff --git a/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm b/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm index 7bf8306e..b060ab4d 100644 --- a/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm +++ b/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm @@ -116,7 +116,7 @@ tcl::namespace::eval punk::repl::codethread { #review/test catch {package require punk::ns} - catch {package rquire punk::repl} + catch {package require punk::repl} #variable xyz diff --git a/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/bootsupport/modules/punk/zip-0.1.1.tm index 96350c0b..97bbe591 100644 --- a/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -420,7 +420,11 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::Addentry - @cmd -name punk::zip::Addentry -help "Add a single file at 'path' to open channel 'zipchan' + @cmd -name punk::zip::Addentry\ + -summary\ + "Add zip-entry for file at 'path'"\ + -help\ + "Add a single file at 'path' to open channel 'zipchan' return a central directory file record" @opts -comment -default "" -help "An optional comment specific to the added file" @@ -543,7 +547,7 @@ tcl::namespace::eval punk::zip { puts -nonewline $zipchan $ddesc } } - + #PK\x01\x02 Cdentral directory file header #set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3 set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems) @@ -565,7 +569,10 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::mkzip @cmd -name punk::zip::mkzip\ - -help "Create a zip archive in 'filename'" + -summary\ + "Create a zip archive in 'filename'."\ + -help\ + "Create a zip archive in 'filename'" @opts -offsettype -default "archive" -choices {archive file}\ -help "zip offsets stored relative to start of entire file or relative to start of zip-archive diff --git a/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/bootsupport/modules/punkcheck-0.1.0.tm index a4113c45..50bcc2f8 100644 --- a/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -243,14 +243,10 @@ namespace eval punkcheck { } method get_targets_exist {} { set punkcheck_folder [file dirname [$o_installer get_checkfile]] + #puts stdout "### punkcheck glob -dir $punkcheck_folder -tails {*}$o_targets" + #targets can be paths such as punk/mix/commandset/module-0.1.0.tm - glob can search levels below supplied -dir set existing [glob -nocomplain -dir $punkcheck_folder -tails {*}$o_targets] - #set existing [list] - #foreach t $o_targets { - # if {[file exists [file join $punkcheck_folder $t]]} { - # lappend existing $t - # } - #} return $existing } method end {} { diff --git a/src/bootsupport/modules/shellfilter-0.2.tm b/src/bootsupport/modules/shellfilter-0.2.tm new file mode 100644 index 00000000..61120a63 --- /dev/null +++ b/src/bootsupport/modules/shellfilter-0.2.tm @@ -0,0 +1,3329 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + variable is_enabled 0 + + proc disable {} { + variable is_enabled + set is_enabled 0 + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} + } + + proc enable {} { + variable is_enabled + set is_enabled 1 + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc ::shellfilter::log::open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc ::shellfilter::log::write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc ::shellfilter::log::write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc ::shellfilter::log::close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } + if {[catch {package require shellthread}]} { + shellfilter::log::disable + } else { + shellfilter::log::enable + } + +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + #method flush {ch} { + # return "" + #} + method flush {transform_handle} { + #puts stdout "" + #review - just clear o_encbuf and emit nothing? + #we wouldn't have a value there if it was convertable from the channel encoding? + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #test with set x [string repeat " \U1f6c8" 2043] + #or + #test with set x [string repeat " \U1f6c8" 683] + #most windows terminals (at least) may emit two unrecognised chars "??" at the end + + #Our goal with the while loop here is to avoid encoding conversion errors + #the source of the bogus chars in terminals is unclear. + #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. + + #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + foreach v $o_datavars { + append $v $stringdata + } + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_encbuf + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {transform_handle bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $stringdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write flush finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set o_encbuf "" + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + #set logdata [encoding convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return + } + } + + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $stringdata + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit + #todo kitty graphics \x1b_G... + #todo iterm graphics + + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_encbuf ;#buffering for partial encoding bytes + variable o_colour + variable o_do_colour + variable o_do_colourlist + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered ;#buffering for partial ansi codes + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence + #while some terminals may handle these extended attributes even when merged - we need to cater for those that + #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without + #affecting the interpretation of the other codes. + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_colourlist {} + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_encbuf "" + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + + + #todo - track when in sixel,iterm,kitty graphics data - can be very large + method Trackcodes {chunk} { + #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) + #e.g [a+ reset reset] (0;0m vs 0;m) + + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # append emit $o_do_colour$pt$o_do_normal + # #append emit $pt + #} else { + # append emit $pt + #} + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$trailing_pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$trailing_pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$trailing_pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + # append emit $o_do_colour$trailing_pt$o_do_normal + #} else { + # append emit $trailing_pt + #} + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #REVIEW - this holding a buffer without emitting as we go is ugly. + # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. + # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. + + + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { + #string index in first part of && clause to avoid some unneeded scans of whole string for this test + #we can't use 'string last' - as we need to know only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal + #set emit [string range $buf 0 end-1] + set buf "" + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + set buf "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } else { + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + set o_buffered "" + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } + } + } + if {$emit_anyway} { + #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. + + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # set emit $o_do_colour$buf$o_do_normal + #} else { + # set emit $buf + #} + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set inputbytes $o_buffered$o_encbuf + set emit [tcl::encoding::convertto $o_enc $inputbytes] + set o_buffered "" + set o_encbuf "" + return $emit + } + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + set streaminfo [my Trackcodes $stringdata] + set emit [dict get $streaminfo emit] + + #review - wrapping already done in Trackcodes + #if {[dict get $streaminfo stacksize] == 0} { + # #no ansi on the stack - we can wrap + # #review + # set outstring "$o_do_colour$emit$o_do_normal" + #} else { + #} + #if {[llength $o_codestack]} { + # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit + #} else { + # set outstring $emit + #} + #set outstring $emit + + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $emit] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + namespace export {[a-z]*} + namespace ensemble create + #todo - implement as oo ? + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tabletitle "$pipecount pipelines active" + set t [textblock::class::table new $tabletitle] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + #JMNJMN 2025 review! + #show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + + #JMNJMN + #show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + if {[catch { + ::shellfilter::log::open $tag {-syslog ""} + } err]} { + #e.g safebase interp can't load required modules such as shellthread (or Thread) + puts stderr "shellfilter::show_pipeline cannot open log" + return + } + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if {$experiment} { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {[llength $args] % 2} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.2 +}] diff --git a/src/bootsupport/modules/textblock-0.1.3.tm b/src/bootsupport/modules/textblock-0.1.3.tm index 31995bfe..d9858980 100644 --- a/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/bootsupport/modules/textblock-0.1.3.tm @@ -137,11 +137,31 @@ tcl::namespace::eval textblock { return " -choices \{$choices\} -help {algorithm choice $choicemsg} " } } + namespace eval argdoc { + tcl::namespace::import ::punk::ansi::a+ + # -- --- --- --- --- + #non colour SGR codes + # we can use these directly via ${$I} etc without marking a definition with @dynamic + #This is because they don't need to change when colour switched on and off. + set I [a+ italic] + set NI [a+ noitalic] + set B [a+ bold] + set N [a+ normal] + # -- --- --- --- --- + proc example {str} { + set str [string trimleft $str \n] + set block [punk::ansi::ansiwrap Web-gray [textblock::frame -ansibase [a+ Web-gray bold white] -ansiborder [a+ black White] -boxlimits {hl} -type block $str]] + set result [textblock::bookend_lines $block [a] "[a defaultbg] [a]"] + #puts $result + return $result + } + } + # hash_algorithm -optional 1 -choices {${[::textblock::argdoc::hash_algorithm_choices]}} -help\ # "algorithm choice" namespace eval argdoc { - set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {$[::textblock::argdoc::hash_algorithm_choices_and_help]} + set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {${[::textblock::argdoc::hash_algorithm_choices_and_help]}} punk::args::define { @dynamic @id -id ::textblock::use_hash @@ -154,7 +174,6 @@ tcl::namespace::eval textblock { } } proc use_hash {args} { - #set argd [punk::args::get_by_id ::textblock::use_hash $args] set argd [punk::args::parse $args withid ::textblock::use_hash] variable use_hash if {![dict exists $argd received hash_algorithm]} { @@ -2294,7 +2313,8 @@ tcl::namespace::eval textblock { #JMN #spanned_parts are all built with textblock::frame - therefore uniform-width lines - can use join_basic - set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + #set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + set spanned_frame [textblock::join_basic_raw {*}$spanned_parts] if {$spans_to_rhs} { if {$cidx == 0} { @@ -2363,7 +2383,8 @@ tcl::namespace::eval textblock { } else { #this_span == 1 - set spanned_frame [textblock::join_basic -- $header_cell_startspan] + #set spanned_frame [textblock::join_basic -- $header_cell_startspan] + set spanned_frame [textblock::join_basic_raw $header_cell_startspan] } @@ -3992,7 +4013,8 @@ tcl::namespace::eval textblock { set body_build "" } else { #body blocks should not be ragged - so can use join_basic - set body_build [textblock::join_basic -- {*}$body_blocks] + #set body_build [textblock::join_basic -- {*}$body_blocks] + set body_build [textblock::join_basic_raw {*}$body_blocks] } if {$headerheight > 0} { set table [tcl::string::cat $header_build \n $body_build] @@ -4149,7 +4171,6 @@ tcl::namespace::eval textblock { proc periodic {args} { #For an impressive interactive terminal app (javascript) # see: https://github.com/spirometaxas/periodic-table-cli - #set opts [dict get [punk::args::get_by_id ::textblock::periodic $args] opts] set opts [dict get [punk::args::parse $args withid ::textblock::periodic] opts] set opt_return [tcl::dict::get $opts -return] if {[tcl::dict::get $opts -forcecolour]} { @@ -4446,7 +4467,7 @@ tcl::namespace::eval textblock { proc list_as_table {args} { set FRAMETYPES [textblock::frametypes] - set argd [punk::args::get_by_id ::textblock::list_as_table $args] + set argd [punk::args::parse $args withid ::textblock::list_as_table] set opts [dict get $argd opts] set received [dict get $argd received] @@ -4644,7 +4665,8 @@ tcl::namespace::eval textblock { if {[tcl::string::last \n $charblock] >= 0} { if {$blockwidth > 1} { #set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]] ;#building a repeated "|> command arg" list to evaluate as a pipeline. (from before textblock::join could take arbitrary num of blocks ) - set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + #set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + set row [textblock::join_basic_raw {*}[lrepeat $blockwidth $charblock]] } else { set row $charblock } @@ -4694,7 +4716,7 @@ tcl::namespace::eval textblock { } proc testblock {args} { - set argd [punk::args::get_by_id ::textblock::testblock $args] + set argd [punk::args::parse $args withid ::textblock::testblock] set colour [dict get $argd values colour] set size [dict get $argd opts -size] @@ -4762,7 +4784,8 @@ tcl::namespace::eval textblock { if {"noreset" in $colour} { return [textblock::join_basic -ansiresets 0 -- {*}$clist] } else { - return [textblock::join_basic -- {*}$clist] + #return [textblock::join_basic -- {*}$clist] + return [textblock::join_basic_raw {*}$clist] } } elseif {"rainbow" in $colour} { #direction must be horizontal @@ -5019,19 +5042,20 @@ tcl::namespace::eval textblock { -width ""\ -overflow 0\ -within_ansi 0\ + -return block\ ] #known_samewidth of empty string means we don't know either way, 0 is definitely 'ragged', 1 is definitely homogenous #review!? #-within_ansi means after a leading ansi code when doing left pad on all but last line #-within_ansi means before a trailing ansi code when doing right pad on all but last line - set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0?" foreach {k v} $args { switch -- $k { - -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi { + -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi - -return { tcl::dict::set opts $k $v } default { + set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0? ?-return block|list?" error "textblock::pad unrecognised option '$k'. Usage: $usage" } } @@ -5177,96 +5201,110 @@ tcl::namespace::eval textblock { set line_len 0 set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad foreach {pt ansi} $parts { - if {$pt ne ""} { - set has_nl [expr {[tcl::string::last \n $pt]>=0}] - if {$has_nl} { + if {$pt eq ""} { + #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes + lappend line_chunks "" + } elseif {[tcl::string::last \n $pt]==-1} { + lappend line_chunks $pt + if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + incr line_len [punk::char::grapheme_width_cached $pt] ;#memleak - REVIEW + } + } else { + #set has_nl [expr {[tcl::string::last \n $pt]>=0}] + #if {$has_nl} { set pt [tcl::string::map [list \r\n \n] $pt] set partlines [split $pt \n] - } else { - set partlines [list $pt] - } - set last [expr {[llength $partlines]-1}] - set p 0 - foreach pl $partlines { - lappend line_chunks $pl + #} else { + # set partlines [list $pt] + #} + #set last [expr {[llength $partlines]-1}] + #set p -1 + foreach pl [lrange $partlines 0 end-1] { + #incr p + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. #incr line_len [punk::char::ansifreestring_width $pl] + #if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + # incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + #} + #do padding if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + set missing [expr {$width - $line_len}] + } else { + set missing [expr {$width - $datawidth}] } - if {$p != $last} { - #do padding - if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { - set missing [expr {$width - $line_len}] - } else { - set missing [expr {$width - $datawidth}] - } - if {$missing > 0} { - #commonly in a block - many lines will have the same pad - cache based on missing + if {$missing > 0} { + #commonly in a block - many lines will have the same pad - cache based on missing - #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars - if {[tcl::dict::exists $pad_cache $missing]} { - set pad [tcl::dict::get $pad_cache $missing] + #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] } else { - set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width - if {!$pad_has_ansi} { - set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] - } else { - set base [tcl::string::repeat " " $missing] - set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] - } - dict set pad_cache $missing $pad + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] } - switch -- $which-$opt_withinansi { - r-0 { - lappend line_chunks $pad - } - r-1 { - if {[lindex $line_chunks end] eq ""} { - set line_chunks [linsert $line_chunks end-2 $pad] - } else { - lappend line_chunks $pad - } - } - r-2 { + dict set pad_cache $missing $pad + } + switch -- $which-$opt_withinansi { + r-0 { + lappend line_chunks $pad + } + r-1 { + if {[lindex $line_chunks end] eq ""} { + set line_chunks [linsert $line_chunks end-2 $pad] + } else { lappend line_chunks $pad } - l-0 { - set line_chunks [linsert $line_chunks 0 $pad] + } + r-2 { + lappend line_chunks $pad + } + l-0 { + set line_chunks [linsert $line_chunks 0 $pad] + } + l-1 { + if {[lindex $line_chunks 0] eq ""} { + set line_chunks [linsert $line_chunks 2 $pad] + } else { + set line_chunks [linsert $line_chunks 0 $pad] } - l-1 { + } + l-2 { + if {$lnum == 0} { if {[lindex $line_chunks 0] eq ""} { set line_chunks [linsert $line_chunks 2 $pad] } else { set line_chunks [linsert $line_chunks 0 $pad] } - } - l-2 { - if {$lnum == 0} { - if {[lindex $line_chunks 0] eq ""} { - set line_chunks [linsert $line_chunks 2 $pad] - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } + } else { + set line_chunks [linsert $line_chunks 0 $pad] } } } - lappend lines [::join $line_chunks ""] - set line_chunks [list] - set line_len 0 - incr lnum } - incr p + lappend lines [::join $line_chunks ""] + set line_chunks [list] + set line_len 0 + incr lnum } - } else { - #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes - lappend line_chunks "" + #deal with last part zzz of xxx\nyyy\nzzz - not yet a complete line + set pl [lindex $partlines end] + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. + if {$pl ne "" && ($known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq "")} { + incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + } + } #don't let trailing empty ansi affect the line_chunks length if {$ansi ne ""} { - lappend line_chunks $ansi ;#don't update line_len - review - ansi codes with visible content? + lappend line_chunks $ansi ;#don't update line_len + #- review - ansi codes with visible content? + #- There shouldn't be any, even though for example some terminals display PM content + #e.g OSC 8 is ok as it has the uri 'inside' the ansi sequence, but that's ok because the displayable part is outside and is one of our pt values from split_codes. } } #pad last line @@ -5325,7 +5363,11 @@ tcl::namespace::eval textblock { } } lappend lines [::join $line_chunks ""] - return [::join $lines \n] + if {[tcl::dict::get $opts -return] eq "block"} { + return [::join $lines \n] + } else { + return $lines + } } #left insertion into a list resulting from punk::ansi::ta::split_codes or split_codes_single @@ -5566,7 +5608,7 @@ tcl::namespace::eval textblock { #join without regard to each line length in a block (no padding added to make each block uniform) proc ::textblock::join_basic {args} { - set argd [punk::args::get_by_id ::textblock::join_basic $args] + set argd [punk::args::parse $args withid ::textblock::join_basic] set ansiresets [tcl::dict::get $argd opts -ansiresets] set blocks [tcl::dict::get $argd values blocks] @@ -5602,6 +5644,33 @@ tcl::namespace::eval textblock { } return [::join $outlines \n] } + proc ::textblock::join_basic_raw {args} { + #no options. -*, -- are legimate blocks + set blocklists [lrepeat [llength $args] ""] + set blocklengths [lrepeat [expr {[llength $args]+1}] 0] ;#add 1 to ensure never empty - used only for rowcount max calc + set i -1 + foreach b $args { + incr i + if {[punk::ansi::ta::detect $b]} { + #-ansireplays 1 quite expensive e.g 7ms in 2024 + set blines [punk::lib::lines_as_list -ansireplays 1 -ansiresets auto -- $b] + } else { + set blines [split $b \n] + } + lset blocklengths $i [llength $blines] + lset blocklists $i $blines + } + set rowcount [tcl::mathfunc::max {*}$blocklengths] + set outlines [lrepeat $rowcount ""] + for {set r 0} {$r < $rowcount} {incr r} { + set row "" + foreach blines $blocklists { + append row [lindex $blines $r] + } + lset outlines $r $row + } + return [::join $outlines \n] + } proc ::textblock::join_basic2 {args} { #@cmd -name textblock::join_basic -help "Join blocks line by line but don't add padding on each line to enforce uniform width. # Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner @@ -5686,9 +5755,12 @@ tcl::namespace::eval textblock { } set idx 0 - set blocklists [list] + #set blocklists [list] + set blocklists [lrepeat [llength $blocks] ""] set rowcount 0 + set bidx -1 foreach b $blocks { + incr bidx #we need the width of a rendered block for per-row renderline calls or padding #we may as well use widthinfo to also determine raggedness state to pass on to pad function #set bwidth [width $b] @@ -5705,18 +5777,21 @@ tcl::namespace::eval textblock { if {[punk::ansi::ta::detect $b]} { # - we need to join to use pad - even though we then need to immediately resplit REVIEW (make line list version of pad?) set replay_block [::join [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] \n] - set bl [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $replay_block -return lines -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } else { #each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi - set bl [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $b -return lines -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } - set rowcount [expr {max($rowcount,[llength $bl])}] - lappend blocklists $bl + set rowcount [expr {max($rowcount,[llength $blines])}] + #lappend blocklists $bl + lset blocklists $bidx $blines set width($idx) $bwidth incr idx } - set outlines [list] + set outlines [lrepeat $rowcount ""] for {set r 0} {$r < $rowcount} {incr r} { set row "" for {set c 0} {$c < [llength $blocklists]} {incr c} { @@ -5726,7 +5801,8 @@ tcl::namespace::eval textblock { } append row $cell } - lappend outlines $row + #lappend outlines $row + lset outlines $r $row } return [::join $outlines \n] } @@ -5910,7 +5986,7 @@ tcl::namespace::eval textblock { set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] set ipunks [overtype::renderspace -width [textblock::width $punks] [punk::ansi::enable_inverse]$punks] - set testblock [textblock::testblock 15 rainbow] + set testblock [textblock::testblock -size 15 rainbow] set contents $ansi\n[textblock::join -- " " $table " " $punks " " $testblock " " $ipunks " " $punks] set framed [textblock::frame -checkargs 0 -type arc -title [a+ cyan]Compositing[a] -subtitle [a+ red]ANSI[a] -ansiborder [a+ web-orange] $contents] } @@ -6206,9 +6282,11 @@ tcl::namespace::eval textblock { set spec [string map [list $::textblock::frametypes] { @id -id ::textblock::framedef @cmd -name textblock::framedef\ + -summary "Return frame graphical elements as a dictionary."\ -help "Return a dict of the elements that make up a frame border. May return a subset of available elements based on memberglob values." - + @leaders -min 0 -max 0 + @opts -joins -default "" -type list\ -help "List of join directions, any of: up down left right or those combined with another frametype e.g left-heavy down-light." @@ -6216,7 +6294,7 @@ tcl::namespace::eval textblock { -help "-boxonly true restricts results to the corner,vertical and horizontal box elements It excludes the extra top and side join elements htlj,hlbj,vllj,vlrj." - @values -min 1 + @values -min 1 -max -1 frametype -choices "" -choiceprefix 0 -choicerestricted 0 -type dict\ -help "name from the predefined frametypes or an adhoc dictionary." memberglob -type globstring -optional 1 -multiple 1 -choiceprefix 0 -choicerestricted 0 -choices { @@ -7619,7 +7697,7 @@ tcl::namespace::eval textblock { } -help "Perform an action on the frame cache." } proc frame_cache {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache] set action [dict get $argd values action] variable frame_cache set all_values_dict [dict get $argd values] @@ -7664,7 +7742,7 @@ tcl::namespace::eval textblock { endindex -default "" -type indexexpression } proc frame_cache_display {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache_display $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache_display] variable frame_cache lassign [dict values [dict get $argd values]] startidx endidx set limit "" @@ -7769,75 +7847,93 @@ tcl::namespace::eval textblock { # ${[textblock::frame_samples]} #todo punk::args alias for centre center etc? - punk::args::define { - @dynamic - @id -id ::textblock::frame - @cmd -name "textblock::frame"\ - -help "Frame a block of text with a border." - -checkargs -default 1 -type boolean\ - -help "If true do extra argument checks and - provide more comprehensive error info. - As the argument parser loads around 16 default frame - samples dynamically, this can add add up as each may - take 10s of microseconds. For many-framed tables - and other applications this can add up. - Set false for performance improvement." - -etabs -default 0\ - -help "expanding tabs - experimental/unimplemented." - -type -default light -choices {${[textblock::frametypes]}} -choicerestricted 0 -choicecolumns 8 -type dict\ - -choicelabels { - ${[textblock::frame_samples]} - }\ - -help "Type of border for frame." - -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. - passing an empty string will result in no box, but title/subtitle will still appear if supplied. - ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" - -boxmap -default {} -type dict - -joins -default {} -type list - -title -default "" -type string -regexprefail {\n}\ - -help "Frame title placed on topbar - no newlines. - May contain ANSI - no trailing reset required. - ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing - e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" - -titlealign -default "centre" -choices {left centre right} - -subtitle -default "" -type string -regexprefail {\n}\ - -help "Frame subtitle placed on bottombar - no newlines - May contain Ansi - no trailing reset required." - -subtitlealign -default "centre" -choices {left centre right} - -width -default "" -type int\ - -help "Width of resulting frame including borders. - If omitted or empty-string, the width will be determined automatically based on content." - -height -default "" -type int\ - -help "Height of resulting frame including borders." - -ansiborder -default "" -type ansistring\ - -help "Ansi escape sequence to set border attributes. - ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents - e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" - -ansibase -default "" -type ansistring\ - -help "Default ANSI attributes within frame." - -blockalign -default centre -choices {left right centre}\ - -help "Alignment of the content block within the frame." - -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background - extends within the content block inside the frame. - Has no effect if no ANSI in content." - -textalign -default left -choices {left right centre}\ - -help "Alignment of text within the content block. (centre unimplemented)" - -ellipsis -default 1 -type boolean\ - -help "Whether to show elipsis for truncated content and title/subtitle." - -usecache -default 1 -type boolean - -buildcache -default 1 -type boolean - -crm_mode -default 0 -type boolean\ - -help "Show ANSI control characters within frame contents. - (Control Representation Mode) - Frame width doesn't adapt and content may be truncated - so -width may need to be manually set to display more." + namespace eval argdoc { + punk::args::define { + @dynamic + @id -id ::textblock::frame + @cmd -name "textblock::frame"\ + -summary "Frame a block of content with a border."\ + -help\ + "This command allows content to be framed with various border styles. The content can include + other ANSI codes and unicode characters. Some predefined border types can be selected with + the -type option and the characters can be overridden either in part or in total by supplying + some or all entries in the -boxmap dictionary. + The ${$B}textblock::framedef${$N} command can be used to return a dictionary for a frame type. + Border elements can also be suppressed on chosen sides with -boxlimits. + ANSI colours can be applied to borders or as defaults for the content using -ansiborder and + -ansibase options. + The punk::ansi::a+ function (aliased as a+) can be used to apply ANSI styles. + e.g + frame -type block -ansiborder [a+ blue Red] -ansibase [a+ black Red] \"A\\nB\"" + -checkargs -default 1 -type boolean\ + -help "If true do extra argument checks and + provide more comprehensive error info. + As the argument parser loads around 16 default frame + samples dynamically, this can add add up as each may + take 10s of microseconds. For many-framed tables + and other applications this can add up. + Set false for performance improvement." + -etabs -default 0\ + -help "expanding tabs - experimental/unimplemented." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${[textblock::frametypes]}}\ + -choicerestricted 0 -choicecolumns 8\ + -choicelabels { + ${[textblock::frame_samples]} + }\ + -help "Type of border for frame." + -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. + passing an empty string will result in no box, but title/subtitle will still appear if supplied. + ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" + -boxmap -default {} -type dict + -joins -default {} -type list + -title -default "" -type string -regexprefail {\n}\ + -help "Frame title placed on topbar - no newlines. + May contain ANSI - no trailing reset required. + ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing + e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" + -titlealign -default "centre" -choices {left centre right} + -subtitle -default "" -type string -regexprefail {\n}\ + -help "Frame subtitle placed on bottombar - no newlines + May contain Ansi - no trailing reset required." + -subtitlealign -default "centre" -choices {left centre right} + -width -default "" -type int\ + -help "Width of resulting frame including borders. + If omitted or empty-string, the width will be determined automatically based on content." + -height -default "" -type int\ + -help "Height of resulting frame including borders." + -ansiborder -default "" -type ansistring\ + -help "Ansi escape sequence to set border attributes. + ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents + e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" + -ansibase -default "" -type ansistring\ + -help "Default ANSI attributes within frame." + -blockalign -default centre -choices {left right centre}\ + -help "Alignment of the content block within the frame." + -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background + extends within the content block inside the frame. + Has no effect if no ANSI in content." + -textalign -default left -choices {left right centre}\ + -help "Alignment of text within the content block. (centre unimplemented)" + -ellipsis -default 1 -type boolean\ + -help "Whether to show elipsis for truncated content and title/subtitle." + -usecache -default 1 -type boolean + -buildcache -default 1 -type boolean + -crm_mode -default 0 -type boolean\ + -help "Show ANSI control characters within frame contents. + (Control Representation Mode) + Frame width doesn't adapt and content may be truncated + so -width may need to be manually set to display more." - @values -min 0 -max 1 - contents -default "" -type string\ - -help "Frame contents - may be a block of text containing newlines and ANSI. - Text may be 'ragged' - ie unequal line-lengths. - No trailing ANSI reset required. - ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + @values -min 0 -max 1 + contents -default "" -type string\ + -help "Frame contents - may be a block of text containing newlines and ANSI. + Text may be 'ragged' - ie unequal line-lengths. + No trailing ANSI reset required. + ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + } } #options before content argument - which is allowed to be absent @@ -7886,7 +7982,8 @@ tcl::namespace::eval textblock { if {[lindex $args end-1] eq "--"} { set contents [lpop optlist end] set has_contents 1 - lpop optlist end ;#drop the end-of-opts flag + #lpop optlist end + ledit optlist end end;#drop the end-of-opts flag } else { set optlist $args set contents "" @@ -7928,7 +8025,6 @@ tcl::namespace::eval textblock { #never need to checkargs if only one argument supplied even if it looks like an option - as it will be treated as data to frame if {[llength $args] != 1 && (!$opts_ok || $check_args)} { #as frame is called a lot within table building - checking args can have a *big* impact on final performance. - #set argd [punk::args::get_by_id ::textblock::frame $args] set argd [punk::args::parse $args withid ::textblock::frame] set opts [dict get $argd opts] set contents [dict get $argd values contents] @@ -8530,7 +8626,8 @@ tcl::namespace::eval textblock { #puts "frame--->ansiwrap -rawansi [ansistring VIEW $opt_ansibase] $cache_inner" if {$opt_ansibase ne ""} { if {[punk::ansi::ta::detect $cache_inner]} { - set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + #set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + set cache_inner [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $cache_inner] } else { set cache_inner "$opt_ansibase$cache_inner\x1b\[0m" } @@ -8561,7 +8658,8 @@ tcl::namespace::eval textblock { #JMN test #assert - lhs, cache_inner, rhs non-ragged - so can use join_basic REVIEW #set cache_body [textblock::join -- {*}$cache_bodyparts] - set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + #set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + set cache_body [textblock::join_basic_raw {*}$cache_bodyparts] append fscached $cache_body #append fs $body @@ -8622,7 +8720,8 @@ tcl::namespace::eval textblock { set contents_has_ansi [punk::ansi::ta::detect $contents] if {$opt_ansibase ne ""} { if {$contents_has_ansi} { - set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + #set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + set contents [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $contents] } else { set contents "$opt_ansibase$contents\x1b\[0m" set contents_has_ansi 1 diff --git a/src/bootsupport/modules_tcl8/include_modules.config b/src/bootsupport/modules_tcl8/include_modules.config index aca4c02e..c65f2a8a 100644 --- a/src/bootsupport/modules_tcl8/include_modules.config +++ b/src/bootsupport/modules_tcl8/include_modules.config @@ -5,6 +5,8 @@ #each entry - base module set bootsupport_modules [list\ modules_tcl8 thread\ - modules_tcl8/thread/platform *\ + modules_tcl8 thread::platform::win32_x86_64_tcl8\ ] +# modules_tcl8/thread/platform *\ + diff --git a/src/bootsupport/modules_tcl8/thread/platform/win32_x86_64_tcl8-2.8.9.tm b/src/bootsupport/modules_tcl8/thread/platform/win32_x86_64_tcl8-2.8.9.tm new file mode 100644 index 00000000..d50bcf4a Binary files /dev/null and b/src/bootsupport/modules_tcl8/thread/platform/win32_x86_64_tcl8-2.8.9.tm differ diff --git a/src/make.tcl b/src/make.tcl index 9809dc62..835fee21 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -181,16 +181,18 @@ set startdir [pwd] # ------------------------------------------------------------------------------------- set bootsupport_module_paths [list] set bootsupport_library_paths [list] +#we always create these lists in order of desired precedence. +# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add if {[file exists [file join $startdir src bootsupport]]} { + lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] ;#more version-specific modules slightly higher in precedence order lappend bootsupport_module_paths [file join $startdir src bootsupport modules] - lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] ;#more version-specific pkgs slightly higher in precedence order lappend bootsupport_library_paths [file join $startdir src bootsupport lib] - lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] } else { - lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_module_paths [file join $startdir bootsupport modules_tcl$::tclmajorv] - lappend bootsupport_library_paths [file join $startdir bootsupport lib] + lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_library_paths [file join $startdir bootsupport lib_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir bootsupport lib] } set bootsupport_paths_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths] { @@ -210,13 +212,13 @@ set sourcesupport_paths_exist 0 #(most?) Modules in src/modules etc should still be runnable directly in certain cases like this where we point to them. if {[file tail $startdir] eq "src"} { #todo - other src 'module' dirs.. - foreach p [list $startdir/modules $startdir/modules_tcl$::tclmajorv $startdir/vendormodules $startdir/vendormodules_tcl$::tclmajorv] { + foreach p [list $startdir/modules_tcl$::tclmajorv $startdir/modules $startdir/vendormodules_tcl$::tclmajorv $startdir/vendormodules] { if {[file exists $p]} { lappend sourcesupport_module_paths $p } } # -- -- -- - foreach p [list $startdir/lib $startdir/lib_tcl$::tclmajorv $startdir/vendorlib $startdir/vendorlib_tcl$::tclmajorv] { + foreach p [list $startdir/lib_tcl$::tclmajorv $startdir/lib $startdir/vendorlib_tcl$::tclmajorv $startdir/vendorlib] { if {[file exists $p]} { lappend sourcesupport_library_paths $p } @@ -273,16 +275,48 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package forget $pkg } } - #tcl::tm::add {*}$original_tm_list {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - #set ::auto_path [list {*}$original_auto_path {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] - tcl::tm::add {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - set ::auto_path [list {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] + #Deliberately omit original_tm_list and original_auto_path + tcl::tm::add {*}[lreverse $bootsupport_module_paths] {*}[lreverse $sourcesupport_module_paths] ;#tm::add works like LIFO. sourcesupport_module_paths end up earliest in resulting tm list. + set ::auto_path [list {*}$sourcesupport_library_paths {*}$bootsupport_library_paths] + } + puts "----> auto_path $::auto_path" + puts "----> tcl::tm::list [tcl::tm::list]" + + #maint: also in punk::repl package + #-------------------------------------------------------- + set libunks [list] + foreach tm_path [tcl::tm::list] { + set punkdir [file join $tm_path punk] + if {![file exists $punkdir]} {continue} + lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] + } + set libunknown "" + set libunknown_version_sofar "" + foreach lib $libunks { + #expecting to be of form libunknown-.tm + set vtail [lindex [split [file tail $lib] -] 1] + set thisver [file rootname $vtail] ;#file rootname x.y.z.tm + if {$libunknown_version_sofar eq ""} { + set libunknown_version_sofar $thisver + set libunknown $lib + } else { + if {[package vcompare $thisver $libunknown_version_sofar] == 1} { + set libunknown_version_sofar $thisver + set libunknown $lib + } + } } - puts "----> auto_path $::auto_path" - - + if {$libunknown ne ""} { + source $libunknown + if {[catch {punk::libunknown::init -caller main.tcl} errM]} { + puts "error initialising punk::libunknown\n$errM" + } + } + #-------------------------------------------------------- #package require Thread + puts "---->tcl_library [info library]" + puts "---->loaded [info loaded]" # - the full repl requires Threading and punk,shellfilter,shellrun to call and display properly. # tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list @@ -297,6 +331,8 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package require punk::lib package require punk::args package require punk::ansi + package require textblock + set package_paths_modified 1 @@ -1217,15 +1253,20 @@ if {$::punkboot::command eq "check"} { #don't exit yet - 2nd part of "check" below package path restore } # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - Order such that bootsupport entries are always higher priority (if same version number - prefer bootsupport) # - This must be done between the two "check" command sections if {$package_paths_modified} { - set tm_list_now [tcl::tm::list] - foreach p $original_tm_list { - if {$p ni $tm_list_now} { + set tm_list_boot [tcl::tm::list] + tcl::tm::remove {*}$tm_list_boot + foreach p [lreverse $original_tm_list] { + if {$p ni $tm_list_boot} { tcl::tm::add $p } } + foreach p [lreverse $tm_list_boot] { + tcl::tm::add $p + } #set ::auto_path [list $bootsupport_lib {*}$original_auto_path] lappend ::auto_path {*}$original_auto_path } @@ -1333,11 +1374,13 @@ if {$::punkboot::command eq "info"} { if {$::punkboot::command eq "shell"} { + puts stderr ">>>>>> loaded:[info loaded]" package require punk package require punk::repl - puts stderr "punk boot shell not implemented - dropping into ordinary punk shell" - #todo - make procs vars etc from this file available? + puts stderr "punk boot shell not implemented - dropping into ordinary punk shell." + + repl::init repl::start stdin @@ -1504,7 +1547,7 @@ if {$::punkboot::command eq "bootsupport"} { proc modfile_sort {p1 p2} { lassign [split [file rootname $p1] -] _ v1 - lassign [split [file rootname $p1] -] _ v2 + lassign [split [file rootname $p2] -] _ v2 package vcompare $v1 $v2 } proc bootsupport_localupdate {projectroot} { @@ -1543,7 +1586,10 @@ if {$::punkboot::command eq "bootsupport"} { set module_subpath [string map [list :: /] [namespace qualifiers $modulematch]] set srclocation [file join $projectroot $relpath $module_subpath] #puts stdout "$relpath $modulematch $module_subpath $srclocation" - if {[string first - $modulematch]} { + #we must always glob using the dash - or we will match libraries that are suffixes of others + #bare lib.tm with no version is not valid. + if {[string first - $modulematch] != -1} { + #version or part thereof is specified. set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]*.tm] } else { set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]-*.tm] @@ -1566,6 +1612,7 @@ if {$::punkboot::command eq "bootsupport"} { #review set copy_files $pkgmatches } + #if a file added manually to target dir - there will be no .punkcheck record - will be detected as changed foreach cfile $copy_files { set srcfile [file join $srclocation $cfile] set tgtfile [file join $targetroot $module_subpath $cfile] @@ -1574,6 +1621,8 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_init INSTALL $tgtfile $boot_event targetset_addsource $srcfile #---------- + # + #puts "bootsuport target $tgtfile record size: [dict size [$boot_event targetset_last_complete]]" if {\ [llength [dict get [$boot_event targetset_source_changes] changed]]\ || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ diff --git a/src/modules/punk/libunknown-0.1.tm b/src/modules/punk/libunknown-0.1.tm index 5cfe3710..82fb87a9 100644 --- a/src/modules/punk/libunknown-0.1.tm +++ b/src/modules/punk/libunknown-0.1.tm @@ -849,19 +849,21 @@ tcl::namespace::eval punk::libunknown { dict for {pkg versiond} $refresh_dict { set versions [dict keys $versiond] + puts stderr "---->pkg:$pkg versions: $versions" foreach searchpath $ordered_searchpaths { set addedinfo [dict get $dict_added $searchpath] set vidx -1 foreach v $versions { incr vidx if {[dict exists $addedinfo $pkg $v]} { - ledit versions $vidx $vidx + ledit versions $vidx $vidx ;incr vidx -1 ;#maintain vidx as index into current state of $versions - not original state the foreach operates across. set iscript [dict get $addedinfo $pkg $v scr] #todo - find the iscript in the '$epoch pkg epochs added paths' lists and determine os vs dev vs internal #(scanning for path directly in the ifneeded script for pkgs is potentially error prone) #for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway) - if {[package ifneeded $pkg $v] ne $iscript} { - #puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath" + set justaddedscript [package ifneeded $pkg $v] + if {$justaddedscript ne $iscript} { + puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions" package ifneeded $pkg $v $iscript #dict set pkgvdone $pkg $v 1 } @@ -887,10 +889,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - #puts stderr "--> pkg epoch $prev_e -> $current_e" - #puts stderr "args: $args" - #puts stderr "last_auto: $last_auto_path" - #puts stderr "auto_path: $auto_path" + puts stderr "--> pkg epoch $prev_e -> $current_e" + puts stderr "args: $args" + puts stderr "last_auto: $last_auto_path" + puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries @@ -1108,7 +1110,7 @@ tcl::namespace::eval punk::libunknown { if {[string match ::* $pkg]} { error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'" } - set versions [package versions $pkg] + set versions [lsort -command {package vcompare} [package versions $pkg]] if {[llength $versions] == 0} { set v [package provide $pkg] } @@ -1519,9 +1521,25 @@ tcl::namespace::eval punk::libunknown { set pkg_row $added set tm_epoch [dict get $epoch tm current] - set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + #set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + set added [dict get $epoch tm epochs $tm_epoch added] + set rows [list] + dict for {path pkgs} $added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + append c3 "$v $data" \n + } + } + set r [list $c1 $c2 $c3] + lappend rows $r + } set title "TM epoch $tm_epoch - added" - set added [textblock::frame -title $title $tm_added] + #set added [textblock::frame -title $title $tm_added] + set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows] set tm_row $added diff --git a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm index 9af513b4..28b6e98a 100644 --- a/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/loadedlib-999999.0a1.0.tm @@ -53,11 +53,6 @@ namespace eval punk::mix::commandset::loadedlib { #REVIEW - this doesn't result in full scans catch {package require frobznodule666} ;#ensure pkg system has loaded/searched for everything - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } set packages [package names] set matches [list] foreach search $searchstrings { @@ -85,11 +80,7 @@ namespace eval punk::mix::commandset::loadedlib { # set versions $v #} } - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {$opt_highlight} { set v [package provide $m] if {$v ne ""} { @@ -188,11 +179,6 @@ namespace eval punk::mix::commandset::loadedlib { } proc info {libname} { - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } catch {package require $libname 1-0} ;#ensure pkg system has loaded/searched - using unsatisfiable version range set pkgsknown [package names] if {[set posn [lsearch $pkgsknown $libname]] >= 0} { @@ -201,11 +187,7 @@ namespace eval punk::mix::commandset::loadedlib { puts stderr "Package not found as available library/module - check tcl::tm::list and \$auto_path" } set versions [package versions [lindex $libname 0]] - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {![llength $versions]} { puts stderr "No version numbers found for library/module $libname" return false diff --git a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm index cae34e31..8cc6e5c5 100644 --- a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm @@ -592,10 +592,23 @@ namespace eval punk::mix::commandset::project { namespace export * namespace path [namespace parent] + punk::args::define { + @id -id ::punk::mix::commandset::project::collection::_default + @cmd -name "punk::mix::commandset::project::collection::_default"\ + -summary\ + "List projects under fossil managment."\ + -help\ + "List projects under fossil management, showing fossil db location and number of checkouts" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * + } #e.g imported as 'projects' - proc _default {{glob {}} args} { + proc _default {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::collection::_default] + set globlist [dict get $argd values glob] + #*** !doctools - #[call [fun _default] [arg glob] [opt {option value...}]] + #[call [fun _default] [arg glob...]] #[para]List projects under fossil management, showing fossil db location and number of checkouts #[para]The glob argument is optional unless option/value pairs are also supplied, in which case * should be explicitly supplied #[para]glob restricts output based on the name of the fossil db file e.g s* for all projects beginning with s @@ -604,7 +617,7 @@ namespace eval punk::mix::commandset::project { #[para] punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection #[para]Will result in the command being available as projects package require overtype - set db_projects [lib::get_projects $glob] + set db_projects [lib::get_projects {*}$globlist] set col1items [lsearch -all -inline -index 0 -subindices $db_projects *] set col2items [lsearch -all -inline -index 1 -subindices $db_projects *] set checkouts [lsearch -all -inline -index 2 -subindices $db_projects *] @@ -1012,12 +1025,21 @@ namespace eval punk::mix::commandset::project { #consider using punk::cap to enable multiple template-substitution providers with their own set of tagnames and/or tag wrappers, where substitution providers are all run return [string cat % $tagname %] } - #get project info only by opening the central confg-db - #(will not have proper project-name etc) - proc get_projects {{globlist {}} args} { - if {![llength $globlist]} { - set globlist [list *] - } + punk::args::define { + @id -id ::punk::mix::commandset::project::lib::get_projects + @cmd -name punk::mix::commandset::project::lib::get_projects\ + -summary\ + "List projects referred to by central fossil config-db."\ + -help\ + "Get project info only by opening the central fossil config-db + (will not have proper project-name etc)" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * -optional 1 + } + proc get_projects {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::lib::get_projects] + set globlist [dict get $argd values glob] + set fossil_prog [auto_execok fossil] set configdb [punk::repo::fossil_get_configdb] diff --git a/src/modules/punk/repl-999999.0a1.0.tm b/src/modules/punk/repl-999999.0a1.0.tm index 0295f142..d9e0390a 100644 --- a/src/modules/punk/repl-999999.0a1.0.tm +++ b/src/modules/punk/repl-999999.0a1.0.tm @@ -3362,7 +3362,7 @@ namespace eval repl { #work around bug in safe base which won't load Tcl libs that have deeper nesting #(also affects tcllib page/plugins folder) - set termversions [package versions term] + set termversions [lsort -command {package vcompare} [package versions term]] set termv [lindex $termversions end] if {$termv ne ""} { set path [lindex [package ifneeded term $termv] end] ;#assuming path at end of something like "source .../term.tcl" diff --git a/src/modules/punkcheck-0.1.0.tm b/src/modules/punkcheck-0.1.0.tm index a4113c45..50bcc2f8 100644 --- a/src/modules/punkcheck-0.1.0.tm +++ b/src/modules/punkcheck-0.1.0.tm @@ -243,14 +243,10 @@ namespace eval punkcheck { } method get_targets_exist {} { set punkcheck_folder [file dirname [$o_installer get_checkfile]] + #puts stdout "### punkcheck glob -dir $punkcheck_folder -tails {*}$o_targets" + #targets can be paths such as punk/mix/commandset/module-0.1.0.tm - glob can search levels below supplied -dir set existing [glob -nocomplain -dir $punkcheck_folder -tails {*}$o_targets] - #set existing [list] - #foreach t $o_targets { - # if {[file exists [file join $punkcheck_folder $t]]} { - # lappend existing $t - # } - #} return $existing } method end {} { diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index 9809dc62..835fee21 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.basic/src/make.tcl @@ -181,16 +181,18 @@ set startdir [pwd] # ------------------------------------------------------------------------------------- set bootsupport_module_paths [list] set bootsupport_library_paths [list] +#we always create these lists in order of desired precedence. +# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add if {[file exists [file join $startdir src bootsupport]]} { + lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] ;#more version-specific modules slightly higher in precedence order lappend bootsupport_module_paths [file join $startdir src bootsupport modules] - lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] ;#more version-specific pkgs slightly higher in precedence order lappend bootsupport_library_paths [file join $startdir src bootsupport lib] - lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] } else { - lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_module_paths [file join $startdir bootsupport modules_tcl$::tclmajorv] - lappend bootsupport_library_paths [file join $startdir bootsupport lib] + lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_library_paths [file join $startdir bootsupport lib_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir bootsupport lib] } set bootsupport_paths_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths] { @@ -210,13 +212,13 @@ set sourcesupport_paths_exist 0 #(most?) Modules in src/modules etc should still be runnable directly in certain cases like this where we point to them. if {[file tail $startdir] eq "src"} { #todo - other src 'module' dirs.. - foreach p [list $startdir/modules $startdir/modules_tcl$::tclmajorv $startdir/vendormodules $startdir/vendormodules_tcl$::tclmajorv] { + foreach p [list $startdir/modules_tcl$::tclmajorv $startdir/modules $startdir/vendormodules_tcl$::tclmajorv $startdir/vendormodules] { if {[file exists $p]} { lappend sourcesupport_module_paths $p } } # -- -- -- - foreach p [list $startdir/lib $startdir/lib_tcl$::tclmajorv $startdir/vendorlib $startdir/vendorlib_tcl$::tclmajorv] { + foreach p [list $startdir/lib_tcl$::tclmajorv $startdir/lib $startdir/vendorlib_tcl$::tclmajorv $startdir/vendorlib] { if {[file exists $p]} { lappend sourcesupport_library_paths $p } @@ -273,16 +275,48 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package forget $pkg } } - #tcl::tm::add {*}$original_tm_list {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - #set ::auto_path [list {*}$original_auto_path {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] - tcl::tm::add {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - set ::auto_path [list {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] + #Deliberately omit original_tm_list and original_auto_path + tcl::tm::add {*}[lreverse $bootsupport_module_paths] {*}[lreverse $sourcesupport_module_paths] ;#tm::add works like LIFO. sourcesupport_module_paths end up earliest in resulting tm list. + set ::auto_path [list {*}$sourcesupport_library_paths {*}$bootsupport_library_paths] + } + puts "----> auto_path $::auto_path" + puts "----> tcl::tm::list [tcl::tm::list]" + + #maint: also in punk::repl package + #-------------------------------------------------------- + set libunks [list] + foreach tm_path [tcl::tm::list] { + set punkdir [file join $tm_path punk] + if {![file exists $punkdir]} {continue} + lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] + } + set libunknown "" + set libunknown_version_sofar "" + foreach lib $libunks { + #expecting to be of form libunknown-.tm + set vtail [lindex [split [file tail $lib] -] 1] + set thisver [file rootname $vtail] ;#file rootname x.y.z.tm + if {$libunknown_version_sofar eq ""} { + set libunknown_version_sofar $thisver + set libunknown $lib + } else { + if {[package vcompare $thisver $libunknown_version_sofar] == 1} { + set libunknown_version_sofar $thisver + set libunknown $lib + } + } } - puts "----> auto_path $::auto_path" - - + if {$libunknown ne ""} { + source $libunknown + if {[catch {punk::libunknown::init -caller main.tcl} errM]} { + puts "error initialising punk::libunknown\n$errM" + } + } + #-------------------------------------------------------- #package require Thread + puts "---->tcl_library [info library]" + puts "---->loaded [info loaded]" # - the full repl requires Threading and punk,shellfilter,shellrun to call and display properly. # tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list @@ -297,6 +331,8 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package require punk::lib package require punk::args package require punk::ansi + package require textblock + set package_paths_modified 1 @@ -1217,15 +1253,20 @@ if {$::punkboot::command eq "check"} { #don't exit yet - 2nd part of "check" below package path restore } # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - Order such that bootsupport entries are always higher priority (if same version number - prefer bootsupport) # - This must be done between the two "check" command sections if {$package_paths_modified} { - set tm_list_now [tcl::tm::list] - foreach p $original_tm_list { - if {$p ni $tm_list_now} { + set tm_list_boot [tcl::tm::list] + tcl::tm::remove {*}$tm_list_boot + foreach p [lreverse $original_tm_list] { + if {$p ni $tm_list_boot} { tcl::tm::add $p } } + foreach p [lreverse $tm_list_boot] { + tcl::tm::add $p + } #set ::auto_path [list $bootsupport_lib {*}$original_auto_path] lappend ::auto_path {*}$original_auto_path } @@ -1333,11 +1374,13 @@ if {$::punkboot::command eq "info"} { if {$::punkboot::command eq "shell"} { + puts stderr ">>>>>> loaded:[info loaded]" package require punk package require punk::repl - puts stderr "punk boot shell not implemented - dropping into ordinary punk shell" - #todo - make procs vars etc from this file available? + puts stderr "punk boot shell not implemented - dropping into ordinary punk shell." + + repl::init repl::start stdin @@ -1504,7 +1547,7 @@ if {$::punkboot::command eq "bootsupport"} { proc modfile_sort {p1 p2} { lassign [split [file rootname $p1] -] _ v1 - lassign [split [file rootname $p1] -] _ v2 + lassign [split [file rootname $p2] -] _ v2 package vcompare $v1 $v2 } proc bootsupport_localupdate {projectroot} { @@ -1543,7 +1586,10 @@ if {$::punkboot::command eq "bootsupport"} { set module_subpath [string map [list :: /] [namespace qualifiers $modulematch]] set srclocation [file join $projectroot $relpath $module_subpath] #puts stdout "$relpath $modulematch $module_subpath $srclocation" - if {[string first - $modulematch]} { + #we must always glob using the dash - or we will match libraries that are suffixes of others + #bare lib.tm with no version is not valid. + if {[string first - $modulematch] != -1} { + #version or part thereof is specified. set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]*.tm] } else { set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]-*.tm] @@ -1566,6 +1612,7 @@ if {$::punkboot::command eq "bootsupport"} { #review set copy_files $pkgmatches } + #if a file added manually to target dir - there will be no .punkcheck record - will be detected as changed foreach cfile $copy_files { set srcfile [file join $srclocation $cfile] set tgtfile [file join $targetroot $module_subpath $cfile] @@ -1574,6 +1621,8 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_init INSTALL $tgtfile $boot_event targetset_addsource $srcfile #---------- + # + #puts "bootsuport target $tgtfile record size: [dict size [$boot_event targetset_last_complete]]" if {\ [llength [dict get [$boot_event targetset_source_changes] changed]]\ || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm index 40366143..b97d1b4e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm @@ -321,6 +321,7 @@ namespace eval argparsingtest { punk::args::define { @id -id ::argparsingtest::test1_punkargs2 @cmd -name argtest4 -help "test of punk::args::parse comparative performance" + @leaders -min 0 -max 0 @opts -anyopts 0 -return -default string -type string -frametype -default \uFFEF -type string @@ -333,10 +334,10 @@ namespace eval argparsingtest { -1 -default 1 -type boolean -2 -default 2 -type integer -3 -default 3 -type integer - @values + @values -min 0 -max 0 } proc test1_punkargs2 {args} { - set argd [punk::args::get_by_id ::argparsingtest::test1_punkargs2 $args] + set argd [punk::args::parse $args withid ::argparsingtest::test1_punkargs2] return [tcl::dict::get $argd opts] } @@ -494,6 +495,38 @@ namespace eval argparsingtest { }]] return $argd } + proc test_multiline2 {args} { + set t3 [textblock::frame t3] + set argd [punk::args::parse $args withdef { + -template1 -default { + ****** + * t1 * + ****** + } + -template2 -default { ------ + ****** + * t2 * + ******} + -template3 -default {$t3} + #substituted or literal values with newlines - no autoindent applied - caller will have to pad appropriately + -template3b -default { + ${$t3} + ----------------- + ${$t3} + abc\ndef + } + -template4 -default "****** + * t4 * + ******" + -template5 -default " + a + ${$t3} + c + " + -flag -default 0 -type boolean + }] + return $argd + } #proc sample1 {p1 n args} { # #*** !doctools diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config index afd1e8f2..226e17de 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config @@ -46,6 +46,7 @@ set bootsupport_modules [list\ modules punkcheck\ modules punkcheck::cli\ modules punk::aliascore\ + modules punk::ansi::colourmap\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -61,6 +62,7 @@ set bootsupport_modules [list\ modules punk::fileline\ modules punk::docgen\ modules punk::lib\ + modules punk::libunknown\ modules punk::mix\ modules punk::mix::base\ modules punk::mix::cli\ diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm index 5d76af04..d6a9c932 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/pattern-1.2.4.tm @@ -1,1285 +1,1285 @@ -#PATTERN -# - A prototype-based Object system. -# -# Julian Noble 2003 -# License: Public domain -# - -# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. -# -# -# Pattern uses a mixture of class-based and prototype-based object instantiation. -# -# A pattern object has 'properties' and 'methods' -# The system makes a distinction between them with regards to the access syntax for write operations, -# and yet provides unity in access syntax for read operations. -# e.g >object . myProperty -# will return the value of the property 'myProperty' -# >ojbect . myMethod -# will return the result of the method 'myMethod' -# contrast this with the write operations: -# set [>object . myProperty .] blah -# >object . myMethod blah -# however, the property can also be read using: -# set [>object . myProperty .] -# Note the trailing . to give us a sort of 'reference' to the property. -# this is NOT equivalent to -# set [>object . myProperty] -# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property -# i.e it is equivalent in this case to: set blah - -#All objects are represented by a command, the name of which contains a leading ">". -#Any commands in the interp which use this naming convention are assumed to be a pattern object. -#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) - -#All user-added properties & methods of the wrapped object are accessed -# using the separator character "." -#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." -# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) -# you would use the 'Create' metamethod on the pattern object like so: -# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject -# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties -# of the object it was created from. ( - - -#The use of the access-syntax separator character "." allows objects to be kept -# 'clean' in the sense that the only methods &/or properties that can be called this way are ones -# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax -# so you are free to implement your own 'Create' method on your object that doesn't conflict with -# the metamethod. - -#Chainability (or how to violate the Law of Demeter!) -#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other -# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference -# structure, without the need to regress to enter matching brackets as is required when using -# standard TCL command syntax. -# ie instead of: -# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething -# we can use: -# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething -# -# This separates out the object-traversal syntax from the TCL command syntax. - -# . is the 'traversal operator' when it appears between items in a commandlist -# . is the 'reference operator' when it is the last item in a commandlist -# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. -# It marks breaks in the multidimensional structure that correspond to how the data is stored. -# e.g obj . arraydata x y , x1 y1 z1 -# represents an element of a 5-dimensional array structured as a plane of cubes -# e.g2 obj . arraydata x y z , x1 y1 -# represents an element of a 5-dimensional array structured as a cube of planes -# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 -# .. is the 'meta-traversal operator' when it appears between items in a commandlist -# .. is the 'meta-info operator'(?) when it is the last item in a commandlist - - -#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing -# implement iStacks & pStacks (interface stacks & pattern stacks) - -#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 - - -#------------------------------------------------------------ -# System objects. -#------------------------------------------------------------ -#::p::-1 ::p::internals::>metaface -#::p::0 ::p::ifaces::>null -#::p::1 ::>pattern -#------------------------------------------------------------ - -#TODO - -#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) - - -#CHANGES -#2018-09 - v 1.2.2 -# varied refactoring -# Changed invocant datastructure curried into commands (the _ID_ structure) -# Changed MAP structure to dict -# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) -# updated test suites -#2018-08 - v 1.2.1 -# split ::p::predatorX functions into separate files (pkgs) -# e.g patternpredator2-1.0.tm -# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken -# -#2017-08 - v 1.1.6 Fairly big overhaul -# New predator function using coroutines -# Added bang operator ! -# Fixed Constructor chaining -# Added a few tests to test::pattern -# -#2008-03 - preserve ::errorInfo during var writes - -#2007-11 -#Major overhaul + new functionality + new tests v 1.1 -# new dispatch system - 'predator'. -# (preparing for multiple interface stacks, multiple invocants etc) -# -# -#2006-05 -# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. -# -#2005-12 -# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. -# -# Fixed so that PatternVariable default applied on Create. -# -# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: -# - heading towards multiple-interface objects -# -#2005-10-28 -# 1.0.8.1 passes 80/80 tests -# >object .. Destroy - improved cleanup of interfaces & namespaces. -# -#2005-10-26 -# fixes to refsync (still messy!) -# remove variable traces on REF vars during .. Destroy -# passes 76/76 -# -#2005-10-24 -# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. -# 1.0.8.0 now passes 75/76 -# -#2005-10-19 -# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) -# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) -# 1.0.8.0 (passes 74/76) -# tests now in own package -# usage: -# package require test::pattern -# test::p::list -# test::p::run ?nameglob? ?-version ? -# -#2005-09?-12 -# -# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. -# fixed @next@ so that destination method resolved at interface compile time instead of call time -# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. -# (before, the overlay only occured when '.. Method' was used to override.) -# -# -# miscellaneous tidy-ups -# -# 1.0.7.8 (passes 71/73) -# -#2005-09-10 -# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value -# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. -# -#2005-09-07 -# bugfix indexed write to list property -# bugfix Variable default value -# 1.0.7.7 (passes 70/72) -# fails: -# arrayproperty.test - array-entire-reference -# properties.test - property_getter_filter_via_ObjectRef -# -#2005-04-22 -# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) -# -# 1.0.7.4 -# -#2004-11-05 -# basic PropertyRead implementation (non-indexed - no tests!) -# -#2004-08-22 -# object creation speedups - (pattern::internals::obj simplified/indirected) -# -#2004-08-17 -# indexed property setter fixes + tests -# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) -# -#2004-08-16 -# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) -# -#2004-08-15 -# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) -# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger -# - also trigger on curried traces to indexed properties i.e list and array elements. -# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. -# -# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] -# -#2004-08-05 -# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) -# -# fix + add tests to support method & property of same name. (method precedence) -# -#2004-08-04 -# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) -# -# 1.0.7.1 -# use objectref array access to read properties even when some props unset; + test -# unset property using array access on object reference; + test -# -# -#2004-07-21 -# object reference changes - array property values appear as list value when accessed using upvared array. -# bugfixes + tests - properties containing lists (multidimensional access) -# -#1.0.7 -# -#2004-07-20 -# fix default property value append problem -# -#2004-07-17 -# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods -# ( -# -#2004-06-18 -# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. -# -#2004-06-05 -# change argsafety operator to be anything with leading - -# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' -# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, -# the entire dash-prefixed operator is also passed in as an argument. -# e.g >object . doStuff -window . -# will call the doStuff method with the 2 parameters -window . -# >object . doStuff - . -# will call doStuff with single parameter . -# >object . doStuff - -window . -# will result in a reference to the doStuff method with the argument -window 'curried' in. -# -#2004-05-19 -#1.0.6 -# fix so custom constructor code called. -# update Destroy metamethod to unset $self -# -#1.0.4 - 2004-04-22 -# bug fixes regarding method specialisation - added test -# -#------------------------------------------------------------ - -package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] - - -namespace eval pattern::util { - - # Generally better to use 'package require $minver-' - # - this only gives us a different error - proc package_require_min {pkg minver} { - if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { - package require $pkg - } else { - error "Package pattern requires package $pkg of at least version $minver. Available: $available" - } - } -} - -package require patterncmd 1.2.4- -package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) - - - -#package require cmdline -package require overtype - -#package require md5 ;#will be loaded if/when needed -#package require md4 -#package require uuid - - - - - -namespace eval pattern { - variable initialised 0 - - - if 0 { - if {![catch {package require twapi_base} ]} { - #twapi is a windows only package - #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. - # If available - windows seems to provide a fast uuid generator.. - #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) - # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) - interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok - } else { - #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) - # (e.g 200usec 2018 corei9) - #(with or without tcllibc?) - #very first call is extremely slow though - 3.5seconds on 2018 corei9 - package require uuid - interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate - } - #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) - } - - -} - - - - - - -namespace eval p { - #this is also the interp alias namespace. (object commands created here , then renamed into place) - #the object aliases are named as incrementing integers.. !todo - consider uuids? - variable ID 0 - namespace eval internals {} - - - #!?? - #namespace export ?? - variable coroutine_instance 0 -} - -#------------------------------------------------------------------------------------- -#review - what are these for? -#note - this function is deliberately not namespaced -# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features -proc process_pattern_aliases {object args} { - set o [namespace tail $object] - interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] - interp alias {} process_method_$o {} [$object .. Method .] - interp alias {} process_constructor_$o {} [$object .. Constructor .] -} -#------------------------------------------------------------------------------------- - - - - -#!store all interface objects here? -namespace eval ::p::ifaces {} - - - -#K combinator - see http://wiki.tcl.tk/1923 -#proc ::p::K {x y} {set x} -#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] - - - - - - - - -proc ::p::internals::(VIOLATE) {_ID_ violation_script} { - #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] - set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] - - if {![dict get $processed explicitvars]} { - #no explicit var statements - we need the implicit ones - set self [set ::p::${_ID_}::(self)] - set IFID [lindex [set $self] 1 0 end] - #upvar ::p::${IFID}:: self_IFINFO - - - set varDecls {} - set vlist [array get ::p::${IFID}:: v,name,*] - set _k ""; set v "" - if {[llength $vlist]} { - append varDecls "upvar #0 " - foreach {_k v} $vlist { - append varDecls "::p::\${_ID_}::$v $v " - } - append varDecls "\n" - } - - #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] - set violation_script $varDecls\n[dict get $processed body] - - #tidy up - unset processed varDecls self IFID _k v - } else { - set violation_script [dict get $processed body] - } - unset processed - - - - - #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. - eval "unset violation_script;$violation_script" -} - - -proc ::p::internals::DestroyObjectsBelowNamespace {ns} { - #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" - - set nsparts [split [string trim [string map {:: :} $ns] :] :] - if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { - #ns not of form ::p::?::_ref - - foreach obj [info commands ${ns}::>*] { - #catch {::p::meta::Destroy $obj} - #puts ">>found object $obj below ns $ns - destroying $obj" - $obj .. Destroy - } - } - - #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] - #foreach tinfo $traces { - # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo - #} - #unset -nocomplain ${ns}::-->PATTERN_ANCHOR - - foreach sub [namespace children $ns] { - ::p::internals::DestroyObjectsBelowNamespace $sub - } -} - - - - -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# - - - - - - - - - -proc ::p::get_new_object_id {} { - tailcall incr ::p::ID - #tailcall ::pattern::new_uuid -} - -#create a new minimal object - with no interfaces or patterns. - -#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} -proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { - - #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" - - if {$OID eq "-2"} { - set OID [::p::get_new_object_id] - #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) - #set OID [pattern::new_uuid] - } - #if $wrapped provided it is assumed to be an existing namespace. - #if {[string length $wrapped]} { - # #??? - #} - - #sanity check - alias must not exist for this OID - if {[llength [interp alias {} ::p::$OID]]} { - error "Object alias '::p::$OID' already exists - cannot create new object with this id" - } - - #system 'varspaces' - - - #until we have a version of Tcl that doesn't have 'creative writing' scope issues - - # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. - # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') - #set o_open 1 - every object is initially also an open interface (?) - #NOTE! comments within namespace eval slow it down. - namespace eval ::p::$OID { - #namespace ensemble create - namespace eval _ref {} - namespace eval _meta {} - namespace eval _iface { - variable o_usedby; - variable o_open 1; - array set o_usedby [list]; - variable o_varspace "" ; - variable o_varspaces [list]; - variable o_methods [dict create]; - variable o_properties [dict create]; - variable o_variables; - variable o_propertyunset_handlers; - set o_propertyunset_handlers [dict create] - } - } - - #set alias ::p::$OID - - #objectid alis default_method object_command wrapped_namespace - set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] - - #MAP is a dict - set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] - - - - #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token - #we've already checked that ::p::$OID doesn't pre-exist - # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias - #interp alias {} ::p::$OID {} ::p::internals::predator $MAP - - - # _ID_ structure - set invocants_dict [dict create this [list $INVOCANTDATA] ] - #puts stdout "New _ID_structure: $interfaces_dict" - set _ID_ [dict create i $invocants_dict context ""] - - - interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ - #rename the command into place - thus the alias & the command name no longer match! - rename ::p::$OID $cmd - - set ::p::${OID}::_meta::map $MAP - - # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something - interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ - - #set p2 [string map {> ?} $cmd] - #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ - - - #trace add command $cmd delete "$cmd .. Destroy ;#" - #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" - - trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" - #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) - - #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" - - - #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" - #trace add command $cmd delete "puts deleting$cmd ;#" - #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" - - - #puts "--> new_object returning map $MAP" - return $MAP -} - - - - -#>x .. Create >y -# ".." is special case equivalent to "._." -# (whereas in theory it would be ".default.") -# "." is equivalent to ".default." is equivalent to ".default.default." (...) - -#>x ._. Create >y -#>x ._.default. Create >y ??? -# -# - -# create object using 'blah' as source interface-stack ? -#>x .blah. .. Create >y -#>x .blah,_. ._. Create .iStackDestination. >y - - - -# -# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] -# the 1st item, blah in this case becomes the 'default' iStack. -# -#>x .*. -# cast to object with all iStacks -# -#>x .*,!_. -# cast to object with all iStacks except _ -# -# --------------------- -#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' -# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. -# -#eg1: >x & >y . some_multi_method arg arg -# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) -# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' -# The invocant signature is thus {these 2} -# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) -# Invocation roles can be specified in the call using the @ operator. -# e.g >x & >y @ points . some_multi_method arg arg -# The invocant signature for this is: {points 2} -# -#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path -# This has the signature {objects n plane 1} where n depends on the length of the list $objects -# -# -# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. -# e.g set pointset [>x & >y .] -# We can now call multimethods on $pointset -# - - - - - - -#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) -proc ::pattern::predatorversion {{ver ""}} { - variable active_predatorversion - set allowed_predatorversions {1 2} - set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions - - if {![info exists active_predatorversion]} { - set first_time_set 1 - } else { - set first_time_set 0 - } - - if {$ver eq ""} { - #get version - if {$first_time_set} { - set active_predatorversions $default_predatorversion - } - return $active_predatorversion - } else { - #set version - if {$ver ni $allowed_predatorversions} { - error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" - } - - if {!$first_time_set} { - if {$active_predatorversion eq $ver} { - #puts stderr "Active predator version is already '$ver'" - #ok - nothing to do - return $active_predatorversion - } else { - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - rename ::p::internals::predator ::p::predator$active_predatorversion - } - } - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - - rename ::p::predator$ver ::p::internals::predator - set active_predatorversion $ver - - return $active_predatorversion - } -} -::pattern::predatorversion 2 - - - - - - - - - - - - -# >pattern has object ID 1 -# meta interface has object ID 0 -proc ::pattern::init args { - - if {[set ::pattern::initialised]} { - if {[llength $args]} { - #if callers want to avoid this error, they can do their own check of $::pattern::initialised - error "pattern package is already initialised. Unable to apply args: $args" - } else { - return 1 - } - } - - #this seems out of date. - # - where is PatternPropertyRead? - # - Object is obsolete - # - Coinjoin, Combine don't seem to exist - array set ::p::metaMethods { - Clone object - Conjoin object - Combine object - Create object - Destroy simple - Info simple - Object simple - PatternProperty simple - PatternPropertyWrite simple - PatternPropertyUnset simple - Property simple - PropertyWrite simple - PatternMethod simple - Method simple - PatternVariable simple - Variable simple - Digest simple - PatternUnknown simple - Unknown simple - } - array set ::p::metaProperties { - Properties object - Methods object - PatternProperties object - PatternMethods object - } - - - - - - #create metaface - IID = -1 - also OID = -1 - # all objects implement this special interface - accessed via the .. operator. - - - - - - set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface - - - #OID = 0 - ::p::internals::new_object ::p::ifaces::>null "" 0 - - #? null object has itself as level0 & level1 interfaces? - #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] - - #null interface should always have 'usedby' members. It should never be extended. - array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array - set ::p::0::_iface::o_open 0 - - set ::p::0::_iface::o_constructor [list] - set ::p::0::_iface::o_variables [list] - set ::p::0::_iface::o_properties [dict create] - set ::p::0::_iface::o_methods [dict create] - set ::p::0::_iface::o_varspace "" - set ::p::0::_iface::o_varspaces [list] - array set ::p::0::_iface::o_definition [list] - set ::p::0::_iface::o_propertyunset_handlers [dict create] - - - - - ############################### - # OID = 1 - # >pattern - ############################### - ::p::internals::new_object ::>pattern "" 1 - - #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] - - - array set ::p::1::_iface::o_usedby [list] ;#'usedby' array - - set _self ::pattern - - #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 - #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 - - - - #1)this object references its interfaces - #lappend ID $IFID $IFID_1 - #lset SELFMAP 1 0 $IFID - #lset SELFMAP 2 0 $IFID_1 - - - #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] - #proc ::>pattern args $body - - - - - ####################################################################################### - #OID = 2 - # >ifinfo interface for accessing interfaces. - # - ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object - set ::p::2::_iface::o_constructor [list] - set ::p::2::_iface::o_variables [list] - set ::p::2::_iface::o_properties [dict create] - set ::p::2::_iface::o_methods [dict create] - set ::p::2::_iface::o_varspace "" - set ::p::2::_iface::o_varspaces [list] - array set ::p::2::_iface::o_definition [list] - set ::p::2::_iface::o_open 1 ;#open for extending - - ::p::ifaces::>2 .. AddInterface 2 - - #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations - #(bootstrap because we can't yet use metaface methods on it) - - - - proc ::p::2::_iface::isOpen.1 {_ID_} { - return $::p::2::_iface::o_open - } - interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 - - proc ::p::2::_iface::isClosed.1 {_ID_} { - return [expr {!$::p::2::_iface::o_open}] - } - interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 - - proc ::p::2::_iface::open.1 {_ID_} { - set ::p::2::_iface::o_open 1 - } - interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 - - proc ::p::2::_iface::close.1 {_ID_} { - set ::p::2::_iface::o_open 0 - } - interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 - - - #proc ::p::2::_iface::(GET)properties.1 {_ID_} { - # set ::p::2::_iface::o_properties - #} - #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 - - #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties - - - #proc ::p::2::_iface::(GET)methods.1 {_ID_} { - # set ::p::2::_iface::o_methods - #} - #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 - #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods - - - - - - #link from object to interface (which in this case are one and the same) - - #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] - #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] - #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] - #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] - - interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen - interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed - interp alias {} ::p::2::open {} ::p::2::_iface::open - interp alias {} ::p::2::close {} ::p::2::_iface::close - - - #namespace eval ::p::2 "namespace export $method" - - ####################################################################################### - - - - - - - set ::pattern::initialised 1 - - - ::p::internals::new_object ::p::>interface "" 3 - #create a convenience object on which to manipulate the >ifinfo interface - #set IF [::>pattern .. Create ::p::>interface] - set IF ::p::>interface - - - #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? - # (or is forcing end user to add their own pStack/iStack ok .. ?) - # - ::p::>interface .. AddPatternInterface 2 ;# - - ::p::>interface .. PatternVarspace _iface - - ::p::>interface .. PatternProperty methods - ::p::>interface .. PatternPropertyRead methods {} { - varspace _iface - var {o_methods alias} - return $alias - } - ::p::>interface .. PatternProperty properties - ::p::>interface .. PatternPropertyRead properties {} { - varspace _iface - var o_properties - return $o_properties - } - ::p::>interface .. PatternProperty variables - - ::p::>interface .. PatternProperty varspaces - - ::p::>interface .. PatternProperty definition - - ::p::>interface .. Constructor {{usedbylist {}}} { - #var this - #set this @this@ - #set ns [$this .. Namespace] - #puts "-> creating ns ${ns}::_iface" - #namespace eval ${ns}::_iface {} - - varspace _iface - var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces - - set o_constructor [list] - set o_variables [list] - set o_properties [dict create] - set o_methods [dict create] - set o_varspaces [list] - array set o_definition [list] - - foreach usedby $usedbylist { - set o_usedby(i$usedby) 1 - } - - - } - ::p::>interface .. PatternMethod isOpen {} { - varspace _iface - var o_open - - return $o_open - } - ::p::>interface .. PatternMethod isClosed {} { - varspace _iface - var o_open - - return [expr {!$o_open}] - } - ::p::>interface .. PatternMethod open {} { - varspace _iface - var o_open - set o_open 1 - } - ::p::>interface .. PatternMethod close {} { - varspace _iface - var o_open - set o_open 0 - } - ::p::>interface .. PatternMethod refCount {} { - varspace _iface - var o_usedby - return [array size o_usedby] - } - - set ::p::2::_iface::o_open 1 - - - - - uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} - #uplevel #0 {package require patternlib} - return 1 -} - - - -proc ::p::merge_interface {old new} { - #puts stderr " ** ** ** merge_interface $old $new" - set ns_old ::p::$old - set ns_new ::p::$new - - upvar #0 ::p::${new}:: IFACE - upvar #0 ::p::${old}:: IFACEX - - if {![catch {set c_arglist $IFACEX(c,args)}]} { - #constructor - #for now.. just add newer constructor regardless of any existing one - #set IFACE(c,args) $IFACEX(c,args) - - #if {![info exists IFACE(c,args)]} { - # #target interface didn't have a constructor - # - #} else { - # # - #} - } - - - set methods [::list] - foreach nm [array names IFACEX m-1,name,*] { - lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) - } - - #puts " *** merge interface $old -> $new ****merging-in methods: $methods " - - foreach method $methods { - if {![info exists IFACE(m-1,name,$method)]} { - #target interface doesn't yet have this method - - set THISNAME $method - - if {![string length [info command ${ns_new}::$method]]} { - - if {![set ::p::${old}::_iface::o_open]} { - #interp alias {} ${ns_new}::$method {} ${ns_old}::$method - #namespace eval $ns_new "namespace export [namespace tail $method]" - } else { - #wait to compile - } - - } else { - error "merge interface - command collision " - } - #set i 2 ??? - set i 1 - - } else { - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - - set i [incr IFACE(m-1,chain,$method)] - - set THISNAME ___system___override_${method}_$i - - #move metadata using subindices for delegated methods - set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) - set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) - set IFACE(mp-$i,$method) $IFACE(mp-1,$method) - - set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) - set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) - - - #set next [::p::next_script $IFID0 $method] - if {![string length [info command ${ns_new}::$THISNAME]]} { - if {![set ::p::${old}::_iface::o_open]} { - interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method - namespace eval $ns_new "namespace export $method" - } else { - #wait for compile - } - } else { - error "merge_interface - command collision " - } - - } - - array set IFACE [::list \ - m-1,chain,$method $i \ - m-1,body,$method $IFACEX(m-1,body,$method) \ - m-1,args,$method $IFACEX(m-1,args,$method) \ - m-1,name,$method $THISNAME \ - m-1,iface,$method $old \ - ] - - } - - - - - - #array set ${ns_new}:: [array get ${ns_old}::] - - - #!todo - review - #copy everything else across.. - - foreach {nm v} [array get IFACEX] { - #puts "-.- $nm" - if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { - set IFACE($nm) $v - } - } - - #!todo -write a test - set ::p::${new}::_iface::o_open 1 - - #!todo - is this done also when iface compiled? - #namespace eval ::p::$new {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place - - return -} - - - - -#detect attempt to treat a reference to a method as a property -proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { -#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" - lassign [lrange $args end-2 end] vtraced vidx op - #NOTE! cannot rely on vtraced as it may have been upvared - - switch -- $op { - write { - error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - unset { - #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace - #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #!todo - don't use vtraced! - trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #pointless raising an error as "Any errors in unset traces are ignored" - #error "cannot unset. $field is a method not a property" - } - read { - error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - array { - error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" - #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" - } - } - - return -} - - - - -#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. -# -# The 'dispatcher' is an object instance's underlying object command. -# - -#proc ::p::make_dispatcher {obj ID IFID} { -# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { -# ::p::@IID@ $methprop @oid@ {*}$args -# }] -# return -#} - - - - -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -#aliased from ::p::${OID}:: -# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something -proc ::p::internals::no_default_method {_ID_ args} { - puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" - lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped - tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" -} - -#force 1 will extend an interface even if shared. (??? why is this necessary here?) -#if IID empty string - create the interface. -proc ::p::internals::expand_interface {IID {force 0}} { - #puts stdout ">>> expand_interface $IID [info level -1]<<<" - if {![string length $IID]} { - #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) - set iid [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$iid - return $iid - } else { - if {[set ::p::${IID}::_iface::o_open]} { - #interface open for extending - shared or not! - return $IID - } - - if {[array size ::p::${IID}::_iface::o_usedby] > 1} { - #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby - - #oops.. shared interface. Copy before specialising it. - set prev_IID $IID - - #set IID [::p::internals::new_interface] - set IID [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$IID - - ::p::internals::linkcopy_interface $prev_IID $IID - #assert: prev_usedby contains at least one other element. - } - - #whether copied or not - mark as open for extending. - set ::p::${IID}::_iface::o_open 1 - return $IID - } -} - -#params: old - old (shared) interface ID -# new - new interface ID -proc ::p::internals::linkcopy_interface {old new} { - #puts stderr " ** ** ** linkcopy_interface $old $new" - set ns_old ::p::${old}::_iface - set ns_new ::p::${new}::_iface - - - - foreach nsmethod [info commands ${ns_old}::*.1] { - #puts ">>> adding $nsmethod to iface $new" - set tail [namespace tail $nsmethod] - set method [string range $tail 0 end-2] ;#strip .1 - - if {![llength [info commands ${ns_new}::$method]]} { - - set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 - - #link from new interface namespace to existing one. - #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) - #!todo? verify? - #- actual link is chainslot to chainslot - interp alias {} ${ns_new}::$method.1 {} $oldhead - - #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? - - - #chainhead pointer within new interface - interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 - - namespace eval $ns_new "namespace export $method" - - #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { - # lappend ${ns_new}::o_methods $method - #} - } else { - if {$method eq "(VIOLATE)"} { - #ignore for now - #!todo - continue - } - - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - #warning - existing chainslot will be completely shadowed by linked method. - # - existing one becomes unreachable. #!todo review!? - - - error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" - - } - } - - - #foreach propinf [set ${ns_old}::o_properties] { - # lassign $propinf prop _default - # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop - # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop - # lappend ${ns_new}::o_properties $propinf - #} - - - set ${ns_new}::o_variables [set ${ns_old}::o_variables] - set ${ns_new}::o_properties [set ${ns_old}::o_properties] - set ${ns_new}::o_methods [set ${ns_old}::o_methods] - set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] - - - set ::p::${old}::_iface::o_usedby(i$new) linkcopy - - - #obsolete.? - array set ::p::${new}:: [array get ::p::${old}:: ] - - - - #!todo - is this done also when iface compiled? - #namespace eval ::p::${new}::_iface {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' - - return -} -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -pattern::init - -return $::pattern::version +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #until we have a version of Tcl that doesn't have 'creative writing' scope issues - + # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. + # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm index 68a14411..6fb185a9 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk-0.1.tm @@ -1,4 +1,4 @@ -#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. +#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. #Built on Tcl of course - because it's the most powerful piece of under-appreciated and alternate-thinking engineering you can plug into. @@ -6,8 +6,8 @@ namespace eval punk { proc lazyload {pkg} { package require zzzload if {[package provide $pkg] eq ""} { - zzzload::pkg_require $pkg - } + zzzload::pkg_require $pkg + } } #lazyload twapi ? @@ -50,9 +50,9 @@ namespace eval punk { } - proc ::punk::auto_execok_original name [info body ::auto_execok] + proc ::punk::auto_execok_original name [info body ::auto_execok] variable better_autoexec - + #set better_autoexec 0 ;#use this var via better_autoexec only #proc ::punk::auto_execok_windows name { # ::punk::auto_execok_original $name @@ -141,6 +141,7 @@ namespace eval punk { } if {[llength [file split $name]] != 1} { + #has a path foreach ext $execExtensions { set file ${name}${ext} if {[file exists $file] && ![file isdirectory $file]} { @@ -164,21 +165,45 @@ namespace eval punk { } foreach var {PATH Path path} { - if {[info exists env($var)]} { - append path ";$env($var)" - } + if {[info exists env($var)]} { + append path ";$env($var)" + } } #change2 - set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { + set lookfor [list $name] + } else { + set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + } + #puts "-->$lookfor" foreach dir [split $path {;}] { + set dir [string trim $dir {\\}] ;#trailing slash will result in a tail such as "/python.exe" #set dir [file normalize $dir] # Skip already checked directories if {[info exists checked($dir)] || ($dir eq "")} { continue } set checked($dir) {} - + + #surprisingly fast + #set matches [glob -nocomplain -dir $dir -types f -tails {*}$lookfor] + ##puts "--dir $dir matches:$matches" + #if {[llength $matches]} { + # set file [file join $dir [lindex $matches 0]] + # #puts "--match0:[lindex $matches 0] file:$file" + # return [set auto_execs($name) [list $file]] + #} + + #what if it's a link? + #foreach match [glob -nocomplain -dir $dir -types f -tail {*}$lookfor] { + # set file [file join $dir $match] + # if {[file exists $file]} { + # return [set auto_execs($name) [list $file]] + # } + #} + + #safest? could be a link? foreach match [glob -nocomplain -dir $dir -tail {*}$lookfor] { set file [file join $dir $match] if {[file exists $file] && ![file isdirectory $file]} { @@ -209,7 +234,7 @@ namespace eval punk { #review - what if punk package reloaded - but ::auto_execs has updated path for winget.exe? #what if we create another interp and use the same ::auto_execs? The appdir won't be detected. #TODO - see if there is a proper windows way to determine where the 'reparse point' apps are installed - + #winget is installed on all modern windows and is an example of the problem this addresses @@ -223,9 +248,9 @@ namespace eval punk { upvar ::punk::can_exec_windowsapp can_exec_windowsapp upvar ::punk::windowsappdir windowsappdir upvar ::punk::cmdexedir cmdexedir - + if {$windowsappdir eq ""} { - #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' + #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' #Tcl (2025) can't exec when given a path to these 0KB files #This path is probably something like C:/Users/username/AppData/Local/Microsoft/WindowsApps if {!([info exists ::env(LOCALAPPDATA)] && @@ -261,13 +286,13 @@ namespace eval punk { return [file join $windowsappdir $name] } if {$cmdexedir eq ""} { - #cmd.exe very unlikely to move + #cmd.exe very unlikely to move set cmdexedir [file dirname [lindex [::punk::auto_execok_windows cmd.exe] 0]] #auto_reset never seems to exist as a command - because auto_reset deletes all commands in the ::auto_index - #anyway.. it has other side effects (affects auto_load) + #anyway.. it has other side effects (affects auto_load) } return "[file join $cmdexedir cmd.exe] /c $name" - } + } return $default_auto }] @@ -279,9 +304,9 @@ namespace eval punk { #repltelemetry cooperation with other packages such as shellrun -#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists +#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists namespace eval punk { - variable repltelemetry_emmitters + variable repltelemetry_emmitters #don't stomp.. even if something created this namespace in advance and is 'cooperating' a bit early if {![info exists repltelemetry_emitters]} { set repltelemetry_emmitters [list] @@ -376,7 +401,7 @@ if {![llength [info commands ::ansistring]]} { package require punk::aliascore ;#mostly punk::lib aliases punk::aliascore::init -force 1 -package require punk::repl::codethread +package require punk::repl::codethread package require punk::config #package require textblock package require punk::console ;#requires Thread @@ -385,6 +410,9 @@ package require punk::winpath ;# for windows paths - but has functions that can package require punk::repo package require punk::du package require punk::mix::base +package require base64 + +package require punk::pipe namespace eval punk { # -- --- --- @@ -415,7 +443,7 @@ namespace eval punk { package require shellfilter package require punkapp package require funcl - + package require struct::list package require fileutil #package require punk::lib @@ -435,8 +463,8 @@ namespace eval punk { #----------------------------------- # todo - load initial debug state from config debug off punk.unknown - debug level punk.unknown 1 - debug off punk.pipe + debug level punk.unknown 1 + debug off punk.pipe debug level punk.pipe 4 debug off punk.pipe.var debug level punk.pipe.var 4 @@ -478,7 +506,7 @@ namespace eval punk { uplevel 1 [list set $varname $obj2] } - interp alias "" strlen "" ::punk::strlen + interp alias "" strlen "" ::punk::strlen interp alias "" str_len "" ::punk::strlen interp alias "" objclone "" ::punk::objclone #proc ::strlen {str} { @@ -487,6 +515,7 @@ namespace eval punk { #proc ::objclone {obj} { # append obj2 $obj {} #} + #----------------------------------------------------------------------------------- #order of arguments designed for pipelining #review - 'piper_' prefix is a naming convention for functions that are ordered for tail-argument pipelining @@ -502,6 +531,351 @@ namespace eval punk { proc ::punk::K {x y} { return $x} + #todo ansigrep? e.g grep using ansistripped value + proc grepstr1 {pattern data} { + set data [string map {\r\n \n} $data] + set lines [split $data \n] + set matches [lsearch -all -regexp $lines $pattern] + set max [lindex $matches end] + set w1 [string length $max] + set result "" + set H [a+ green bold overline] + set R \x1b\[m + foreach m $matches { + set ln [lindex $lines $m] + set ln [regsub -all $pattern $ln $H&$R] + append result [format %${w1}s $m] " $ln" \n + } + set result [string trimright $result \n] + return $result + } + + #---------------------- + #todo - fix overtype + #create test + #overtype::renderline -insert_mode 0 -transparent 1 [a+ green]-----[a] " [a+ underline]x[a]" + #---------------------- + + + punk::args::define { + @id -id ::punk::grepstr + @cmd -name punk::grepstr\ + -summary\ + "Grep for regex pattern in plaintext of supplied (possibly ANSI) string."\ + -help\ + "The grepstr command can find strings in ANSI text even if there are interspersed + ANSI colour codes etc. Even if a word has different coloured/styled letters, the + regex can match the plaintext. (Search is performed on ansistripped text, and then + the matched sections are highlighted and overlayed on the original styled/colourd + input. + If the input string has ANSI movement codes - the resultant text may not be directly + searchable because the parts of a word may be separated by various codes and other + plain text. To search such an input string, the string should first be 'rendered' to + a form where the ANSI only represents SGR styling (and perhaps other non-movement + codes) using something like overtype::renderline or overtype::rendertext." + + @leaders -min 0 -max 0 + @opts + -returnlines -type string -typesynopsis matched|all -default breaksandmatches -choicecolumns 1 -choices {matched all breaksandmatches} -choicelabels { + "matched"\ + " Return only lines that matched." + "breaksandmatches"\ + " Return configured --break= lines in between non-consecutive matches" + "all"\ + " Return all lines. + This has a similar effect to the 'grep' trick of matching on 'pattern|$' + (The $ matches all lines that have an end; ie all lines, but there is no + associated character to which to apply highlighting) + except that when instead using -returnlines all with --line-number, the * + indicator after the linenumber will only be highlighted for lines with matches, + and the following matchcount will indicate zero for non-matching lines." + } + -B|--before-context= -parsekey "--before-context" -default 0 -type integer -typesynopsis num + -C|--context= -parsekey "--context" -default 0 -type integer -typesynopsis num -help\ + "Print num lines of leading and trailing context surrounding each match." + -A|--after-context= -parsekey "--after-context" -default 0 -type integer -typesynopsis num + --break= -type string -default "-- %c%\U2260" -help\ + "When returning matched lines and there is a break in consecutive output, + display the break with the given string. %c% is a placeholder for the + number of lines skipped. + Use empty-string for an empty line as a break display. + grepstr --break= needle $haystacklines + + The unix grep utility commonly uses -- for this indicator. + grepstr --break=-- needle $haystacklines + + Customisation example: + grepstr -n \"--break=(skipped %c% lines)\" needle $haystacklines + " + -ansistrip -type none -help\ + "Strip all ansi codes from the input string before processing. + This is not necessary for regex matching purposes, as the matching is always + performed on the ansistripped characters anyway, but by stripping ANSI, the + result only has the ANSI supplied by the -highlight option." + + #-n|--line-number as per grep utility, except that we include a * for matches + -n|--line-number -type none -help\ + "Each output line is preceded by its relative line number in the file, starting at line 1. + For lines that matched the regex, the line number will be suffixed with a * indicator + with the same highlighting as the matched string(s). + The number of matches in the line immediately follows the * + For lines with no matches the * indicator is present with no highlighting and suffixed + with zeros." + -i|--ignore-case -type none -help\ + "Perform case insensitive matching." + -highlight -type list -typesynopsis ansinames -default {green bold Black underline overline} -help\ + "list of ANSI SGR style codes as supported by and documented in punk::ansi::a?" + -- -type none + @values + pattern -type string -help\ + "regex pattern to match in plaintext portion of ANSI string" + string -type string + } + proc grepstr {args} { + lassign [dict values [punk::args::parse $args withid ::punk::grepstr]] leaders opts values received + set pattern [dict get $values pattern] + set data [dict get $values string] + set do_strip 0 + if {[dict exists $received -ansistrip]} { + set data [punk::ansi::ansistrip $data] + } + set highlight [dict get $opts -highlight] + set opt_returnlines [dict get $opts -returnlines] + set context [dict get $opts --context] ;#int + set beforecontext [dict get $opts --before-context] + set beforecontext [expr {max($beforecontext,$context)}] + set aftercontext [dict get $opts --after-context] + set aftercontext [expr {max($aftercontext,$context)}] + set break [dict get $opts --break] + set ignorecase [dict exists $received --ignore-case] + if {$ignorecase} { + set nocase "-nocase" + } else { + set nocase "" + } + + + if {[dict exists $received --line-number]} { + set do_linenums 1 ;#display lineindex+1 + } else { + set do_linenums 0 + } + + if {[llength $highlight] == 0} { + set H "" + set R "" + } else { + set H [a+ {*}$highlight] + set R \x1b\[m + } + + set data [string map {\r\n \n} $data] + if {![punk::ansi::ta::detect $data]} { + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $lines $pattern] + set result "" + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + #matches|breaksandmatches + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + incr max + } + set w1 [string length $max] + #lineindex is zero based - display of linenums is 1 based + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set ln [regsub -all {*}$nocase -- $pattern $ln $H&$R] ;#n + set matchcount [regexp -all {*}$nocase -- $pattern $ln] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + } else { + if {$do_linenums} { + append col1 "*000" + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + if {$do_linenums} { + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + + } + } else { + set plain [punk::ansi::ansistrip $data] + set plainlines [split $plain \n] + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $plainlines $pattern] + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + #if max index is 9 - linenum will be 10, (99->100 etc) - so add one in case we're on such a boundary. + incr max + } + set w1 [string length $max] + set result "" + set placeholder \UFFEF ;#review + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set plain_ln [lindex $plainlines $lineindex] + set parts [regexp -all {*}$nocase -indices -inline -- $pattern $plain_ln] + set matchcount [llength $parts] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + if {[llength $parts] == 0} { + #This probably can't happen (?) + #If it does.. it's more likely to be an issue with our line index than with regexp + puts stderr "Unexpected regex mismatch in grepstr - line marked with ??? (shouldn't happen)" + set matchshow "??? $ln" + #dict set resultlines $lineindex $show + } else { + set overlay "" + set i 0 + foreach prange $parts { + lassign $prange s e + set prelen [expr {$s - $i}] + append overlay [string repeat $placeholder $prelen] $H[string range $plain_ln $s $e]$R + set i [expr {$e + 1}] + } + set tail [string range $plain_ln $e+1 end] + append overlay [string repeat $placeholder [string length $tail]] + #puts "$overlay" + #puts "$ln" + set rendered [overtype::renderline -transparent $placeholder -insert_mode 0 $ln $overlay] + if {$do_linenums} { + set matchshow "$col1 $rendered" + } else { + set matchshow $rendered + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + dict set resultlines $lineindex $matchshow + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + } else { + if {$do_linenums} { + append col1 "*000" + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + } + } + } + set ordered_resultlines [lsort -integer [dict keys $resultlines]] + set result "" + set i -1 + set do_break 0 + if {$opt_returnlines eq "breaksandmatches"} { + set do_break 1 + } + if {$do_break} { + foreach r $ordered_resultlines { + incr i + if {$r > $i} { + set c [expr {$r - $i}] + append result [string map [list %c% $c] $break] \n + } + append result [dict get $resultlines $r] \n + set i $r + } + if {$i<[llength $lines]-1} { + set c [expr {[llength $lines]-1-$i}] + append result [string map [list %c% $c] $break] \n + } + } else { + foreach r $ordered_resultlines { + append result [dict get $resultlines $r] \n + } + } + set result [string trimright $result \n] + return $result + } + proc stacktrace {} { set stack "Stack trace:\n" for {set i 1} {$i < [info level]} {incr i} { @@ -563,22 +937,24 @@ namespace eval punk { #get last command result that was run through the repl proc ::punk::get_runchunk {args} { - set argd [punk::args::get_dict { + set argd [punk::args::parse $args withdef { @id -id ::punk::get_runchunk + @cmd -name "punk::get_runchunk" -help\ + "experimental" @opts - -1 -optional 1 -type none - -2 -optional 1 -type none + -1 -optional 1 -type none + -2 -optional 1 -type none @values -min 0 -max 0 - } $args] + }] #todo - make this command run without truncating previous runchunks set runchunks [tsv::array names repl runchunks-*] set sortlist [list] foreach cname $runchunks { set num [lindex [split $cname -] 1] - lappend sortlist [list $num $cname] + lappend sortlist [list $num $cname] } - set sorted [lsort -index 0 -integer $sortlist] + set sorted [lsort -index 0 -integer $sortlist] set chunkname [lindex $sorted end-1 1] set runlist [tsv::get repl $chunkname] #puts stderr "--$runlist" @@ -635,10 +1011,10 @@ namespace eval punk { set inopts 1 } else { #leave loop at first nonoption - i should be index of file - break + break } } else { - #leave for next iteration to check + #leave for next iteration to check set inopts 0 } incr i @@ -654,44 +1030,8 @@ namespace eval punk { set ::argc $argc return -code $code $return } - #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ - # - #we can't provide a float comparison suitable for every situation, - #but we pick something reasonable, keep it stable, and document it. - proc float_almost_equal {a b} { - package require math::constants - set diff [expr {abs($a - $b)}] - if {$diff <= $math::constants::eps} { - return 1 - } - set A [expr {abs($a)}] - set B [expr {abs($b)}] - set largest [expr {($B > $A) ? $B : $A}] - return [expr {$diff <= $largest * $math::constants::eps}] - } - #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. - proc boolean_equal {a b} { - #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. - expr {($a && 1) == ($b && 1)} - } - #debatable whether boolean_almost_equal is likely to be surprising or helpful. - #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically - #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? - proc boolean_almost_equal {a b} { - if {[string is double -strict $a]} { - if {[float_almost_equal $a 0]} { - set a 0 - } - } - if {[string is double -strict $b]} { - if {[float_almost_equal $b 0]} { - set b 0 - } - } - #must handle true,no etc. - expr {($a && 1) == ($b && 1)} - } + proc varinfo {vname {flag ""}} { @@ -703,9 +1043,9 @@ namespace eval punk { error "can't read \"$vname\": no such variable" } set inf [shellfilter::list_element_info [list $v]] - set inf [dict get $inf 0] + set inf [dict get $inf 0] if {$flag eq "-v"} { - return $inf + return $inf } set output [dict create] @@ -781,7 +1121,7 @@ namespace eval punk { } else { append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -791,162 +1131,12 @@ namespace eval punk { } return $varlist } - proc splitstrposn {s p} { - if {$p <= 0} { - if {$p == 0} { - list "" $s - } else { - list $s "" - } - } else { - scan $s %${p}s%s - } - } - proc splitstrposn_nonzero {s p} { - scan $s %${p}s%s - } - #split top level of patterns only. - proc _split_patterns_memoized {varspecs} { - set name_mapped [pipecmd_namemapping $varspecs] - set cmdname ::punk::pipecmds::split_patterns::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - set result [_split_patterns $varspecs] - proc $cmdname {} [list return $result] - #debug.punk.pipe.compile {proc $cmdname} 4 - return $result - } - proc _split_patterns {varspecs} { - - set varlist [list] - # @ @@ - list and dict functions - # / level separator - # # list count, ## dict size - # % string functions - # ! not - set var_terminals [list "@" "/" "#" "%" "!" ">" "<"] ;# (> required for insertionspecs at rhs of = & .= ) - #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname - - #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# - #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string - #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' - set in_brackets 0 ;#count depth - set in_atom 0 - #set varspecs [string trimleft $varspecs ,] - set token "" - #if {[string first "," $varspecs] <0} { - # return $varspecs - #} - set first_term -1 - set token_index 0 ;#index of terminal char within each token - set indq 0 - set inesc 0 ;#whether last char was backslash (see also punk::escv) - set prevc "" - set char_index 0 - foreach c [split $varspecs ""] { - if {$indq} { - if {$inesc} { - #puts stderr "inesc adding '$c'" - append token $c - } else { - if {$c eq {"}} { - set indq 0 - } else { - append token $c - } - } - } elseif {$in_atom} { - #ignore dquotes/brackets in atoms - pass through - append token $c - #set nextc [lindex $chars $char_index+1] - if {$c eq "'"} { - set in_atom 0 - } - } elseif {$in_brackets > 0} { - append token $c - if {$c eq ")"} { - incr in_brackets -1 - } - } else { - if {$c eq {"} && !$inesc} { - set indq 1 - } elseif {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - set token "" - set token_index -1 ;#reduce by 1 because , not included in next token - set first_term -1 - } else { - append token $c - switch -exact -- $c { - ' { - set in_atom 1 - } - ( { - incr in_brackets - } - default { - if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index - } - } - } - } - } - set prevc $c - if {$c eq "\\"} { - #review - if {$inesc} { - set inesc 0 - } else { - set token [string range $token 0 end-1] - set inesc 1 - } - } else { - set inesc 0 - } - incr token_index - incr char_index - } - if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - } - return $varlist - } proc _split_var_key_at_unbracketed_comma {varspecs} { set varlist [list] set var_terminals [list "@" "/" "#" "!"] #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# + set protect_terminals [list "^"] ;# e.g sequence ^# #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' set in_brackets 0 @@ -966,27 +1156,17 @@ namespace eval punk { } } else { if {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] + set token "" set token_index -1 ;#reduce by 1 because , not included in next token set first_term -1 } else { append token $c if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index + set first_term $token_index } elseif {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -994,18 +1174,7 @@ namespace eval punk { incr token_index } if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] } return $varlist } @@ -1029,6 +1198,7 @@ namespace eval punk { } else { if {$c eq ","} { if {$first_term > -1} { + #lassign [punk::lib::string_splitbefore $token $first_term] v k set v [string range $token 0 $first_term-1] set k [string range $token $first_term end] ;#key section includes the terminal char lappend varlist [list $v $k] @@ -1041,12 +1211,12 @@ namespace eval punk { } else { if {$first_term == -1} { if {$c in $var_terminals} { - set first_term $token_index + set first_term $token_index } } append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -1067,7 +1237,7 @@ namespace eval punk { proc fp_restructure {selector data} { if {$selector eq ""} { fun=.= {val $input} 0 || abs($offset) >= $len)} { set action ?mismatch-list-index-out-of-range break @@ -1424,7 +1594,7 @@ namespace eval punk { } elseif {$start eq "end"} { #ok } elseif {$do_bounds_check} { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0 || abs($startoffset) >= $len} { set action ?mismatch-list-index-out-of-range @@ -1481,7 +1651,7 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + } else { #keyword 'pipesyntax' at beginning of error message error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] @@ -1513,23 +1683,40 @@ namespace eval punk { return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs] } - #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script + #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script proc destructure_func {selector data} { #puts stderr ".d." set selector [string trim $selector /] - #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position - #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position - - #map some problematic things out of the way in a manner that maintains some transparency - #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} - #The selector forms part of the proc name - set selector_safe [string map [list ? * {$} "" "\x1b\[" "\x1b\]" {[} {]} :: {;} " " \t \n \r ] $selector] + #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position + #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position + + #map some problematic things out of the way in a manner that maintains some transparency + #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} + #The selector forms part of the proc name + #review - compare with pipecmd_namemapping + set selector_safe [string map [list\ + ? \ + * \ + \\ \ + {"} \ + {$} \ + "\x1b\[" \ + "\x1b\]" \ + {[} \ + {]} \ + :: \ + {;} \ + " " \ + \t \ + \n \ + \r \ + ] $selector] set cmdname ::punk::pipecmds::destructure::_$selector_safe if {[info commands $cmdname] ne ""} { return [$cmdname $data] ;# note upvar 2 for stateful v_list_idx to be resolved in _multi_bind_result context } - + set leveldata $data set body [destructure_func_build_procbody $cmdname $selector $data] @@ -1553,8 +1740,8 @@ namespace eval punk { proc destructure_func_build_procbody {cmdname selector data} { set script "" #place selector in comment in script only - if there is an error in selector we pick it up when building the script. - #The script itself should only be returning errors in its action key of the result dictionary - append script \n [string map [list $selector] {# set selector {}}] + #The script itself should only be returning errors in its action key of the result dictionary + append script \n [string map [list $selector] {# set selector {}}] set subindices [split $selector /] append script \n [string map [list [list $subindices]] {# set subindices }] set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break @@ -1562,7 +1749,7 @@ namespace eval punk { #append script \n {set assigned ""} ;#review set active_key_type "" append script \n {# set active_key_type ""} - set lhs "" + set lhs "" #append script \n [tstr {set lhs ${{$lhs}}}] append script \n {set lhs ""} set rhs "" @@ -1582,9 +1769,9 @@ namespace eval punk { #dict 'index' when using stateful @@ etc to iterate over dict instead of by key set tpl_return_mismatch_dict_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-dict-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} set tpl_return_mismatch_dict_key_not_found {return [dict create -assigned $leveldata -action ?mismatch-dict-key-not-found -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} - - if {![string length $selector]} { + + if {![string length $selector]} { #just return $leveldata set script { dict create -assigned $leveldata -action ?match -lhs "" -rhs $leveldata @@ -1598,7 +1785,7 @@ namespace eval punk { #pure numeric keylist - put straight to lindex # #NOTE: this direct access e.g v/0/1/2 doesn't check out of bounds which is at odds with list access containing @ - #We will leave this as a syntax for different (more performant) behaviour + #We will leave this as a syntax for different (more performant) behaviour #- it's potentially a little confusing - but it would be a shame not to have the possibility to take advantage of the lindex deep indexing capability in pattern matching. #TODO - review and/or document # @@ -1625,7 +1812,7 @@ namespace eval punk { # -- --- --- } if {[string match @@* $selector]} { - #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc + #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc set rawkeylist [split $selector /] ;#first key retains @@ - may be just '@@' set keypath [string range $selector 2 end] set keylist [split $keypath /] @@ -1659,11 +1846,11 @@ namespace eval punk { foreach index $subindices { #set index_operation "unspecified" set level_script_complete 0 ;#instead of break - as we can't use data to determine break when building script - set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] + set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] append script \n "# ------- START index:$index subpath:$SUBPATH ------" set lhs $index - append script \n "set lhs $index" - + append script \n "set lhs {$index}" + set assigned "" append script \n {set assigned ""} @@ -1677,21 +1864,21 @@ namespace eval punk { # do_bounds_check shouldn't need to be in script set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning. - #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. + #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. #append script \n {set do_boundscheck 0} switch -exact -- $index { # - @# { #list length set active_key_type "list" if {$get_not} { - lappend INDEX_OPERATIONS not-list + lappend INDEX_OPERATIONS not-list append script \n {# set active_key_type "list" index_operation: not-list} append script \n { if {[catch {llength $leveldata}]} { - #not a list - not-length is true + #not a list - not-length is true set assigned 1 } else { - #is a list - not-length is false + #is a list - not-length is false set assigned 0 } } @@ -1710,7 +1897,7 @@ namespace eval punk { #dict size set active_key_type "dict" if {$get_not} { - lappend INDEX_OPERATIONS not-dict + lappend INDEX_OPERATIONS not-dict append script \n {# set active_key_type "dict" index_operation: not-dict} append script \n { if {[catch {dict size $leveldata}]} { @@ -1733,10 +1920,10 @@ namespace eval punk { } %# { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS string-length append script \n {# set active_key_type "" index_operation: string-length} append script \n {set assigned [string length $leveldata]} @@ -1745,10 +1932,10 @@ namespace eval punk { %%# { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS ansistring-length append script \n {# set active_key_type "" index_operation: ansistring-length} append script \n {set assigned [ansistring length $leveldata]} @@ -1756,7 +1943,7 @@ namespace eval punk { } %str { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%str - not string-get is not supported" } lappend INDEX_OPERATIONS string-get @@ -1767,7 +1954,7 @@ namespace eval punk { %sp { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%sp - not string-space is not supported" } lappend INDEX_OPERATIONS string-space @@ -1778,7 +1965,7 @@ namespace eval punk { %empty { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%empty - not string-empty is not supported" } lappend INDEX_OPERATIONS string-empty @@ -1788,10 +1975,10 @@ namespace eval punk { } @words { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%words - not list-words-from-string is not supported" } - lappend INDEX_OPERATIONS list-words-from-string + lappend INDEX_OPERATIONS list-words-from-string append script \n {# set active_key_type "" index_operation: list-words-from-string} append script \n {set assigned [regexp -inline -all {\S+} $leveldata]} set level_script_complete 1 @@ -1800,10 +1987,10 @@ namespace eval punk { #experimental - leading character based on result not input(?) #input type is string - but output is list set active_key_type "list" - if $get_not { + if {$get_not} { error "!%chars - not list-chars-from-string is not supported" } - lappend INDEX_OPERATIONS list-from_chars + lappend INDEX_OPERATIONS list-from_chars append script \n {# set active_key_type "" index_operation: list-chars-from-string} append script \n {set assigned [split $leveldata ""]} set level_script_complete 1 @@ -1812,7 +1999,7 @@ namespace eval punk { #experimental - flatten one level of list #join without arg - output is list set active_key_type "string" - if $get_not { + if {$get_not} { error "!@join - not list-join-list is not supported" } lappend INDEX_OPERATIONS list-join-list @@ -1824,7 +2011,7 @@ namespace eval punk { #experimental #input type is list - but output is string set active_key_type "string" - if $get_not { + if {$get_not} { error "!%join - not string-join-list is not supported" } lappend INDEX_OPERATIONS string-join-list @@ -1834,7 +2021,7 @@ namespace eval punk { } %ansiview { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiview is not supported" } lappend INDEX_OPERATIONS string-ansiview @@ -1844,7 +2031,7 @@ namespace eval punk { } %ansiviewstyle { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiviewstyle is not supported" } lappend INDEX_OPERATIONS string-ansiviewstyle @@ -1855,23 +2042,23 @@ namespace eval punk { @ { #as this is a stateful list next index operation - we use not (!@) to mean there is no element at the next index (instead of returning the complement ie all elements except next) #This is in contrast to other not operations on indices e.g /!2 which returns all elements except that at index 2 - + #append script \n {puts stderr [uplevel 1 [list info vars]]} #NOTE: #v_list_idx in context of _multi_bind_result - #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) + #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) append script \n {upvar 2 v_list_idx v_list_idx} set active_key_type "list" append script \n {# set active_key_type "list" index_operation: list-get-next} #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3 - #while x@,y@.= is reasonably handy - especially for args e.g $keyglob] { # set active_key_type "dict" index_operation: globkey-get-pairs-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict remove $leveldata {*}$matched] }] @@ -2285,7 +2473,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-pairs append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operations: globkey-get-pairs - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict create] foreach m $matched { dict set assigned $m [dict get $leveldata $m] @@ -2307,7 +2495,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys-not append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict keys [dict remove $leveldata {*}$matched]] }] @@ -2315,7 +2503,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys - set assigned [dict keys $leveldata ] + set assigned [dict keys $leveldata {}] }] } set level_script_complete 1 @@ -2323,7 +2511,7 @@ namespace eval punk { {@k\*@*} - {@K\*@*} { #dict value glob - return keys set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2331,22 +2519,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-keys-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match "" $v]} { + if {![string match {} $v]} { lappend assigned $k } } }] } else { lappend INDEX_OPERATIONS globvalue-get-keys - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys set assigned [list] tcl::dict::for {k v} $leveldata { - if {[string match "" $v]} { + if {[string match {} $v]} { lappend assigned $k } } @@ -2357,7 +2545,7 @@ namespace eval punk { {@.\*@*} { #dict value glob - return pairs set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2365,22 +2553,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-pairs-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { dict set assigned $k $v } } }] } else { lappend INDEX_OPERATIONS globvalue-get-pairs - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $v]} { + if {[string match {} $v]} { dict set assigned $k $v } } @@ -2389,9 +2577,9 @@ namespace eval punk { set level_script_complete 1 } {@V\*@*} - {@v\*@*} { - #dict value glob - return values + #dict value glob - return values set active_key_type dict - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2399,11 +2587,11 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-values-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;# index_operation: globvalue-get-values-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { lappend assigned $v } } @@ -2411,9 +2599,9 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS globvalue-get-values - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;#index_operation: globvalue-get-value - set assigned [dict values $leveldata ] + set assigned [dict values $leveldata ] }] } set level_script_complete 1 @@ -2437,14 +2625,14 @@ namespace eval punk { # set active_key_type "dict" ;# index_operation: globkeyvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $k] || [string match $v]} { + if {[string match {} $k] || [string match {} $v]} { dict set assigned $k $v } } }] } - - error "globkeyvalue-get-pairs todo" + set level_script_complete 1 + puts stderr "globkeyvalue-get-pairs review" } @* { set active_key_type "list" @@ -2483,7 +2671,7 @@ namespace eval punk { append listmsg "Use var@@key to treat value as a dict and retrieve element at key" #append script \n [string map [list $listmsg] {set listmsg ""}] - + #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against @@ -2544,7 +2732,7 @@ namespace eval punk { ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax - ${$assignment_script} + ${$assignment_script} } }] } @@ -2568,7 +2756,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { - ${$assignment_script} + ${$assignment_script} } }] } else { @@ -2577,13 +2765,13 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assignment_script} + ${$assignment_script} } }] } } tail { - #NOTE: /@tail and /tail both do bounds check. This is intentional. + #NOTE: /@tail and /tail both do bounds check. This is intentional. # #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems. @@ -2596,7 +2784,7 @@ namespace eval punk { append script \n "# index_operation listindex-tail" \n lappend INDEX_OPERATIONS listindex-tail set assignment_script {set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero} - } + } append script \n [tstr -return string -allowcommands { if {[catch {llength $leveldata} len]} { #set action ?mismatch-not-a-list @@ -2693,7 +2881,7 @@ namespace eval punk { } raw { #get_not - return nothing?? - #no list checking.. + #no list checking.. if {$get_not} { lappend INDEX_OPERATIONS getraw-not append script \n {set assigned {}} @@ -2748,7 +2936,7 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS list-getpairs } - append script \n [tstr -return string -allowcommands { + append script \n [tstr -return string -allowcommands { if {[catch {dict size $leveldata} dsize]} { #set action ?mismatch-not-a-dict ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2776,7 +2964,7 @@ namespace eval punk { if {[catch {llength $leveldata} len]} { ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } elseif {[string is integer -strict $index]} { @@ -2816,7 +3004,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2847,7 +3035,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} } else { - ${$assign_script} + ${$assign_script} } } }] @@ -2857,7 +3045,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2896,15 +3084,15 @@ namespace eval punk { } elseif {$start eq "end"} { #noop } else { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0} { #e.g end+1 error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } - append script \n [tstr -return string -allowcommands { - set startoffset ${$startoffset} + append script \n [tstr -return string -allowcommands { + set startoffset ${$startoffset} if {abs($startoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2916,7 +3104,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set end ${$end} + set end ${$end} if {$end+1 > $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2932,7 +3120,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set endoffset ${$endoffset} + set endoffset ${$endoffset} if {abs($endoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -3014,13 +3202,13 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + append script \n [string map [list $assign_script] { if {![string match ?mismatch-* $action]} { } }] - + } else { #keyword 'pipesyntax' at beginning of error message #pipesyntax error - no need to even build script - can fail now @@ -3072,7 +3260,7 @@ namespace eval punk { #dict remove can accept non-existent keys.. review do we require not-@?@key to get silence? append script \n [tstr -return string { set assigned [dict remove $leveldata ${$index}] - }] + }] } else { append script \n [tstr -return string -allowcommands { # set active_key_type "dict" @@ -3096,7 +3284,7 @@ namespace eval punk { } incr i_keyindex append script \n "# ------- END index $index ------" - } ;# end foreach + } ;# end foreach @@ -3109,157 +3297,6 @@ namespace eval punk { return $script } - #todo - recurse into bracketed sub parts - #JMN3 - #e.g @*/(x@0,y@2) - proc _var_classify {multivar} { - set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - - #comma seems a natural choice to split varspecs, - #but also for list and dict subelement access - #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) - #so / will indicate subelements e.g @0/1 for lindex $list 0 1 - #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] - set valsource_key_list [_split_patterns_memoized $multivar] - - - - #mutually exclusive - atom/pin - #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin - #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] - #0 - novar - #1 - atom ' - #2 - pin ^ - #3 - boolean & - #4 - integer - #5 - double - #6 - var - #7 - glob (no classifier and contains * or ?) - #8 - numeric - #9 - > (+) - #10 - < (-) - - set var_names [list] - set var_class [list] - set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob - - - set leading_classifiers [list "'" "&" "^" ] - set trailing_classifiers [list + -] - set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] - - foreach v_key $valsource_key_list { - lassign $v_key v key - set vname $v ;#default - set classes [list] - if {$v eq ""} { - lappend var_class [list $v_key 0] - lappend varspecs_trimmed $v_key - } else { - set lastchar [string index $v end] - switch -- $lastchar { - + { - lappend classes 9 - set vname [string range $v 0 end-1] - } - - { - lappend classes 10 - set vname [string range $v 0 end-1] - } - } - set firstchar [string index $v 0] - switch -- $firstchar { - ' { - lappend var_class [list $v_key 1] - #set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - ^ { - lappend classes [list 2] - #use vname - may already have trailing +/- stripped - set vname [string range $vname 1 end] - set secondclassifier [string index $v 1] - switch -- $secondclassifier { - "&" { - #pinned boolean - lappend classes 3 - set vname [string range $v 2 end] - } - "#" { - #pinned numeric comparison instead of string comparison - #e.g set x 2 - # this should match: ^#x.= list 2.0 - lappend classes 8 - set vname [string range $vname 1 end] - } - "*" { - #pinned glob - lappend classes 7 - set vname [string range $v 2 end] - } - } - #todo - check for second tag - & for pinned boolean? - #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. - #while we're at it.. pinned glob would be nice. ^* - #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. - #These all limit the range of varnames permissible - which is no big deal. - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed [list $vname $key] - } - & { - #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. - #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans - #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. - lappend var_class [list $v_key 3] - set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - default { - if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { - lappend var_class [list $v_key 7] ;#glob - #leave vname as the full glob - lappend varspecs_trimmed [list "" $key] - } else { - #scan vname not v - will either be same as v - or possibly stripped of trailing +/- - set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 - #leading . still need to test directly for double - if {[string is double -strict $vname] || [string is double -strict $numtestv]} { - if {[string is integer -strict $numtestv]} { - #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired - #integer test before double.. - #note there is also string is wide (string is wideinteger) for larger ints.. - lappend classes 4 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } else { - #double - #sci notation 1e123 etc - #also large numbers like 1000000000 - even without decimal point - (tcl bignum) - lappend classes 5 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } - } else { - lappend var_class [list $v_key 6] ;#var - lappend varspecs_trimmed $v_key - } - } - } - } - } - lappend var_names $vname - } - - set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] - - proc $cmdname {} [list return $result] - debug.punk.pipe.compile {proc $cmdname} - return $result - } @@ -3269,41 +3306,41 @@ namespace eval punk { #TODO - implement cross-binding (as opposed to overwrite/reassignment) when a var appears multiple times in a pattern/multivar #e.g x@0,x@1 will only match if value at positions 0 & 1 is the same (a form of auto-pinning?) #e.g x,x@0 will only match a single element list - #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) + #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) # non-blocking or - p1||p2 if p1 matches - return p1 and continue pipeline - else match p2 and continue pipeline proc _multi_bind_result {multivar data args} { #puts stdout "---- _multi_bind_result multivar:'$multivar' data:'$data' options:'$args'" - #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 + #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 if {![string length $multivar]} { #treat the absence of a pattern as a match to anything #JMN2 - changed to list based destructuring return [dict create ismatch 1 result $data setvars {} script {}] #return [dict create ismatch 1 result [list $data] setvars {} script {}] } - set returndict [dict create ismatch 0 result "" setvars {}] - set script "" + set returndict [dict create ismatch 0 result "" setvars {}] + set script "" - set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] - set opts [dict merge $defaults $args] - set unset [dict get $opts -unset] - set lvlup [dict get $opts -levelup] - set get_mismatchinfo [dict get $opts -mismatchinfo] + set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] + set opts [dict merge $defaults $args] + set unset [dict get $opts -unset] + set lvlup [dict get $opts -levelup] + set get_mismatchinfo [dict get $opts -mismatchinfo] #first classify into var_returntype of either "pipeline" or "segment" #segment returntype is indicated by leading % - set varinfo [_var_classify $multivar] - set var_names [dict get $varinfo var_names] - set var_class [dict get $varinfo var_class] - set varspecs_trimmed [dict get $varinfo varspecs_trimmed] + set varinfo [punk::pipe::lib::_var_classify $multivar] + set var_names [dict get $varinfo var_names] + set var_class [dict get $varinfo var_class] + set varspecs_trimmed [dict get $varinfo varspecs_trimmed] set var_actions [list] set expected_values [list] #e.g {a = abc} {b set ""} foreach classinfo $var_class vname $var_names { - lassign [lindex $classinfo 0] v + lassign [lindex $classinfo 0] v lappend var_actions [list $v "" ""] ;#varactions keeps original lhs - not trimmed version lappend expected_values [list var $vname spec $v info - lhs - rhs -] ;#code looks for 'info -' to see if changed from default } @@ -3314,7 +3351,7 @@ namespace eval punk { #puts stdout "\n var_class: $var_class\n" # e.g {{x {}} 0} {{y @0} 0} {{'ok @0} 1} {{^v @@key} 2} - + #set varspecs_trimmed [lmap varinfo $var_class {expr {([lindex $varinfo 1] > 0) ? [list [string range [lindex $varinfo 0 0] 1 end] [lindex $varinfo 0 1]] : [lindex $varinfo 0]}}] #puts stdout "\n varspecs_trimmed: $varspecs_trimmed\n" @@ -3329,18 +3366,18 @@ namespace eval punk { #member lists of returndict which will be appended to in the initial value-retrieving loop set returndict_setvars [dict get $returndict setvars] - + set assigned_values [list] #varname action value - where value is value to be set if action is set - #actions: + #actions: # "" unconfigured - assert none remain unconfigured at end # noop no-change # matchvar-set name is a var to be matched # matchatom-set names is an atom to be matched # matchglob-set - # set + # set # question mark versions are temporary - awaiting a check of action vs var_class # e.g ?set may be changed to matchvar or matchatom or set @@ -3355,7 +3392,7 @@ namespace eval punk { # ^var means a pinned variable - compare value of $var to rhs - don't assign # # In this loop we don't set variables - but assign an action entry in var_actions - all with leading question mark. - # as well as adding the data values to the var_actions list + # as well as adding the data values to the var_actions list # # TODO! we may (commonly) encounter same vkey in the pattern - no need to reparse and re-fetch from data! set vkeys_seen [list] @@ -3396,8 +3433,8 @@ namespace eval punk { dict set returndict setvars $returndict_setvars #assigned_values is the ordered list of source elements in the data (rhs) as extracted by each position-spec - #For booleans the final val may later be normalised to 0 or 1 - + #For booleans the final val may later be normalised to 0 or 1 + #assertion all var_actions were set with leading question mark #perform assignments only if matched ok @@ -3424,7 +3461,7 @@ namespace eval punk { debug.punk.pipe.var {vars: [lsearch -all -inline -index 1 $var_class 6]} 5 debug.punk.pipe.var {globs: [lsearch -all -inline -index 1 $var_class 7]} 5 } - + set match_state [lrepeat [llength $var_names] ?] unset -nocomplain v unset -nocomplain nm @@ -3445,7 +3482,7 @@ namespace eval punk { set class_key [lindex $var_class $i 1] - lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan + lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan foreach ck $class_key { switch -- $ck { 1 {set isatom 1} @@ -3473,7 +3510,7 @@ namespace eval punk { ##marking numbers with pin ^ has undetermined meaning. Perhaps force expr matching only? #set isgreaterthan [expr {9 in $class_key}] #set islessthan [expr {10 in $class_key}] - + if {$isatom} { @@ -3502,7 +3539,7 @@ namespace eval punk { # - setting expected_values when match_state is set to 0 is ok except for performance - #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or + #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or #ispin may reclassify as isint,isdouble based on contained value (as they don't have their own classifier char and are unambiguous and require special handling) if {$ispin} { #puts stdout "==>ispin $lhsspec" @@ -3512,7 +3549,7 @@ namespace eval punk { upvar $lvlup $varname the_var #if {![catch {uplevel $lvlup [list ::set $varname]} existingval]} {} if {![catch {set the_var} existingval]} { - + if {$isbool} { #isbool due to 2nd classifier i.e ^& lset expected_values $i [list var $varname spec $lhsspec info test-lhs-bool lhs $existingval rhs $val] @@ -3522,7 +3559,7 @@ namespace eval punk { #isglob due to 2nd classifier ^* lset expected_values $i [list var $varname spec $lhsspec info test-lhs-glob lhs $existingval rhs $val] } elseif {$isnumeric} { - #flagged as numeric by user using ^# classifiers + #flagged as numeric by user using ^# classifiers set testexistingval [join [scan $existingval %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, internal decimal points and sci notation (but not leading .) if {[string is integer -strict $testexistingval]} { set isint 1 @@ -3533,10 +3570,10 @@ namespace eval punk { set isdouble 1 #doubles comparisons use float_almost_equal - so lhs can differ from rhs - for pins we always want to return the normalised lhs ie exactly what is in the var lset assigned_values $i $existingval - + lset expected_values $i [list var $varname spec $lhsspec info test-lhs-double lhs $existingval rhs $val] } else { - #user's variable doesn't seem to have a numeric value + #user's variable doesn't seem to have a numeric value lset match_state $i 0 lset expected_values $i [list var $varname spec $lhsspec info mismatch-lhs-not-numeric lhs $existingval rhs $val] break @@ -3561,7 +3598,7 @@ namespace eval punk { lset expected_values $i [list var $varname spec $lhsspec info failread-$varname lhs ? rhs $val] break } - } + } } @@ -3583,7 +3620,7 @@ namespace eval punk { if {[string index $lhs 0] eq "."} { set testlhs $lhs } else { - set testlhs [join [scan $lhs %lld%s] ""] + set testlhs [join [scan $lhs %lld%s] ""] } if {[string index $val 0] eq "."} { set testval $val @@ -3648,10 +3685,10 @@ namespace eval punk { } } elseif {[string is digit -strict [string trim $val -]] } { #probably a wideint or bignum with no decimal point - #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . + #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . #if we use float_almost_equal they may compare equal. on the other hand expr also does apparently inconsistent thins with comparing integer-like bignums vs similar sized nums with .x at the end. - #2 values further apart can compare equal while int-like ones closer together can compare different. - #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. + #2 values further apart can compare equal while int-like ones closer together can compare different. + #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. #This is basically what we're doing here but with an arguably better (for some purposes!) float comparison. #string comparison can presumably always be used as an alternative. # @@ -3682,7 +3719,7 @@ namespace eval punk { } } } else { - if {[punk::float_almost_equal $testlhs $testval]} { + if {[punk::pipe::float_almost_equal $testlhs $testval]} { lset match_state $i 1 } else { if {$isgreaterthan} { @@ -3709,7 +3746,7 @@ namespace eval punk { } } } else { - #e.g rhs not a number.. + #e.g rhs not a number.. if {$testlhs == $testval} { lset match_state $i 1 } else { @@ -3721,7 +3758,7 @@ namespace eval punk { } elseif {$isdouble} { #dragons (and shimmering) # - # + # if {$ispin} { set existing_expected [lindex $expected_values $i] set lhs [dict get $existing_expected lhs] @@ -3761,7 +3798,7 @@ namespace eval punk { } } else { #float_almost_equal will disagree with expr based on scale.. just enough to allow for example [expr 0.2 + 0.1] to equal 0.3 - whereas expr will declare a mismatch - if {[punk::float_almost_equal $lhs $testval]} { + if {[punk::pipe::float_almost_equal $lhs $testval]} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-float-almost-equal lhs $lhs rhs $val] } else { @@ -3777,7 +3814,7 @@ namespace eval punk { # Also - the setting of the variable x is normalized to 1 or 0 only. (true & false would perhaps be nicer - but 1 & 0 are theoretically more efficient for later comparisons as they can have a pure int rep?.) # todo - consider the booleanString rep. Can/should we return true & false instead and maintain efficiency w.r.t shimmering? # - #punk::boolean_equal $a $b + #punk::pipe::boolean_equal $a $b set extra_match_info "" ;# possible crossbind indication set is_literal_boolean 0 if {$ispin} { @@ -3789,7 +3826,7 @@ namespace eval punk { set lhs [string range $lhsspec 1 end] ;# - strip off & classifier prefix if {![string length $lhs]} { - #empty varname - ok + #empty varname - ok if {[string is boolean -strict $val] || [string is double -strict $val]} { lset match_state $i 1 lset var_actions $i 1 "return-normalised-value" @@ -3813,7 +3850,7 @@ namespace eval punk { set tclvar $lhs if {[string is double $tclvar]} { error "pipesyntax invalid variable name '$tclvar' for boolean in pattern. (subset of legal tcl vars allowed in pattern context)" "_multi_bind_result $multivar $data $args" [list pipesyntax patternvariable invalid_boolean $tclvar] - #proc _multi_bind_result {multivar data args} + #proc _multi_bind_result {multivar data args} } #treat as variable - need to check cross-binding within this pattern group set first_bound [lsearch -index 0 $var_actions $lhsspec] @@ -3846,7 +3883,7 @@ namespace eval punk { #may have already matched above..(for variable) if {[lindex $match_state $i] != 1} { - if {![catch {punk::boolean_almost_equal $lhs $val} ismatch]} { + if {![catch {punk::pipe::boolean_almost_equal $lhs $val} ismatch]} { if {$ismatch} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-boolean-almost-equal$extra_match_info lhs $lhs rhs $val] @@ -3880,11 +3917,11 @@ namespace eval punk { } } elseif {$ispin} { - #handled above.. leave case in place so we don't run else for pins + #handled above.. leave case in place so we don't run else for pins } else { #puts stdout "==> $lhsspec" - #NOTE - pinned var of same name is independent! + #NOTE - pinned var of same name is independent! #ie ^x shouldn't look at earlier x bindings in same pattern #unpinned non-atoms #cross-binding. Within this multivar pattern group only (use pin ^ for binding to result from a previous pattern) @@ -3904,7 +3941,7 @@ namespace eval punk { } default { set first_bound [lsearch -index 0 $var_actions $varname] - #assertion first_bound >=0, we will always find something - usually self + #assertion first_bound >=0, we will always find something - usually self if {$first_bound == $i} { lset match_state $i 1 lset var_actions $i 1 [string range $act 1 end] ;# ?set -> set @@ -3964,7 +4001,7 @@ namespace eval punk { if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { #isvar if {[lindex $var_actions $i 1] eq "set"} { - upvar $lvlup $varname the_var + upvar $lvlup $varname the_var set the_var [lindex $var_actions $i 2] } } @@ -3976,7 +4013,7 @@ namespace eval punk { # if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { # #isvar # lassign $va lhsspec act val - # upvar $lvlup $varname the_var + # upvar $lvlup $varname the_var # if {$act eq "set"} { # set the_var $val # } @@ -3990,7 +4027,8 @@ namespace eval punk { #todo - some way to restrict mismatch info to simple "mismatch" and avoid overhead of verbose message #e.g for within pipeswitch block where mismatches are expected and the reasons are less important than moving on quickly set vidx 0 - set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + #set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + set mismatches [lmap m $match_state v $var_names {expr {$m == 0 ? [list mismatch $v] : [list match $v]}}] set var_display_names [list] foreach v $var_names { if {$v eq ""} { @@ -3999,7 +4037,9 @@ namespace eval punk { lappend var_display_names $v } } - set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + #REVIEW 2025 + #set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0 ? $v : [expr {$m eq "?" ? "?[string repeat { } [expr {[string length $v] -1}]]" : [string repeat " " [string length $v]] }]}}] set msg "\n" append msg "Unmatched\n" append msg "Cannot match right hand side to pattern $multivar\n" @@ -4015,12 +4055,12 @@ namespace eval punk { #6 - var #7 - glob (no classifier and contains * or ?) foreach mismatchinfo $mismatches { - lassign $mismatchinfo status varname + lassign $mismatchinfo status varname if {$status eq "mismatch"} { # varname can be empty string set varclass [lindex $var_class $i 1] set val [lindex $var_actions $i 2] - set e [dict get [lindex $expected_values $i] lhs] + set e [dict get [lindex $expected_values $i] lhs] set type "" if {2 in $varclass} { append type "pinned " @@ -4098,7 +4138,7 @@ namespace eval punk { return [dict get $d result] } } - # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch + # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch proc _handle_bind_result_experimental1 {d} { #set match_caller [info level 2] #debug.punk.pipe {_handle_bind_result match_caller: $match_caller} 9 @@ -4122,55 +4162,43 @@ namespace eval punk { upvar $pipevarname the_pipe set the_pipe $args } - + #pipealias should capture the namespace context of the pipeline so that commands are resolved in the namespace in which the pipealias is created proc pipealias {targetcmd args} { set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] - tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] + tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] } proc pipealias_extract {targetcmd} { set applybody [lindex [interp alias "" $targetcmd] 1 1] #strip off trailing " {*}$args" - return [lrange [string range $applybody 0 end-9] 0 end] + return [lrange [string range $applybody 0 end-9] 0 end] } #although the pipealias2 'concat' alias is cleaner in that the original pipeline can be extracted using list commands - it runs much slower proc pipealias2 {targetcmd args} { - set cmdcopy [punk::objclone $args] + set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] } - #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) - # (for .= and = pipecmds) - proc pipecmd_namemapping {rhs} { - #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. - #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence - #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test - set rhs [string trim $rhs];#ignore all leading & trailing whitespace - set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token - set rhs [tcl::string::map {: ? * } $rhs] - #review - we don't expect other command-incompatible chars such as colon? - return $rhs - } #same as used in unknown func for initial launch - #variable re_assign {^([^\r\n=\{]*)=(.*)} - #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} variable re_assign {^([^ \t\r\n=\{]*)=(.*)} variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #match_assign is tailcalled from unknown - uplevel 1 gets to caller level proc match_assign {scopepattern equalsrhs args} { - #review - :: is legal in atoms! + #review - :: is legal in atoms! if {[string match "*::*" $scopepattern]} { error "match_assign scopepattern '$scopepattern' contains namespace separator '::' - invalid." } #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args" set fulltail $args set cmdns ::punk::pipecmds - set namemapping [pipecmd_namemapping $equalsrhs] + set namemapping [punk::pipe::lib::pipecmd_namemapping $equalsrhs] - #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW + #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW #(we need for example x*= to be available as is via namespace path mechanism (from punk::pipecmds namespace)) set pipecmd ${cmdns}::$scopepattern=$namemapping @@ -4189,10 +4217,10 @@ namespace eval punk { #NOTE: #we need to ensure for case: - #= x=y + #= x=y #that the second arg is treated as a raw value - never a pipeline command - #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = + #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = #debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4 #can match pattern on lhs with a value where pattern is a minilang that can refer to atoms (simple non-whitespace strings), numbers, or varnames (possibly pinned) as well as a trailing spec for position within the data. @@ -4202,7 +4230,7 @@ namespace eval punk { # in our script's handling of args: #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists - #same with lsearch with a string pattern - + #same with lsearch with a string pattern - #wouldn't matter for small lists - but we need to be able to handle large ones efficiently without unneccessary string reps set script [string map [list $scopepattern $equalsrhs] { #script built by punk::match_assign @@ -4210,7 +4238,7 @@ namespace eval punk { #scan for existence of any pipe operator (|*> or <*|) only - we don't need position #all pipe operators must be a single element #we don't first check llength args == 1 because for example: - # x= <| + # x= <| # x= |> #both leave x empty. To assign a pipelike value to x we would have to do: x= <| |> (equiv: set x |>) foreach a $args { @@ -4239,14 +4267,14 @@ namespace eval punk { # as we aren't in a pipleine - there is no data to insert - we proably still need to run _split_equalsrhs to verify the syntax. # review - consider way to turn it off as optimisation for non-pipelined assignment - but generally standard Tcl set could be used for that purpose. # We are probably only here if testing in the repl - in which case the error messages are important. - set var_index_position_list [_split_equalsrhs $equalsrhs] + set var_index_position_list [punk::pipe::lib::_split_equalsrhs $equalsrhs] #we may have an insertion-spec that inserts a literal atom e.g to wrap in "ok" # x='ok'>0/0 data # => {ok data} - # we won't examine for vars as there is no pipeline - ignore + # we won't examine for vars as there is no pipeline - ignore # also ignore trailing * (indicator for variable data to be expanded or not - ie {*}) # we will differentiate between / and @ in the same way that general pattern matching works. - # /x will simply call linsert without reference to length of list + # /x will simply call linsert without reference to length of list # @x will check for out of bounds # # !TODO - sort by position lowest to highest? or just require user to order the pattern correctly? @@ -4259,7 +4287,7 @@ namespace eval punk { #Here, we are not assigning to v1 - but matching the index spec /0 with the data from v1 #ie Y is inserted at position 0 to get A Y #(Note the difference from lhs) - #on lhs v1/1= {X Y} + #on lhs v1/1= {X Y} #would pattern match against the *data* A B and set v1 to B #in this point of an assign (= as opposed to .=) IF we have already determined there is no trailing pipeline @@ -4268,10 +4296,10 @@ namespace eval punk { #eg out= list a $callervar c #or alternatively use .= instead # - #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments + #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments #At the moment - this is handled in the script above by diverting to punk::pipeline to handle #The only vars/data we can possibly have to insert, come from the ] }] - set needs_insertion 0 + set needs_insertion 0 } if {$needs_insertion} { set script2 [punk::list_insertion_script $positionspec segmenttail ] set script2 [string map [list "\$insertion_data" ] $script2] append script $script2 - } + } + - } } - if {![string length $scopepattern]} { + if {![string length $scopepattern]} { append script { return $segmenttail } } else { append script [string map [list $scopepattern] { #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail - set d [punk::_multi_bind_result {} $segmenttail] + set d [punk::_multi_bind_result {} $segmenttail] #return [punk::_handle_bind_result $d] - #maintenance: inlined + #maintenance: inlined if {![dict exists $d result]} { #uplevel 1 [list error [dict get $d mismatch]] #error [dict get $d mismatch] @@ -4356,7 +4384,7 @@ namespace eval punk { tailcall $pipecmd {*}$args } - #return a script for inserting data into listvar + #return a script for inserting data into listvar #review - needs updating for list-return semantics of patterns? proc list_insertion_script {keyspec listvar {data }} { set positionspec [string trimright $keyspec "*"] @@ -4384,15 +4412,15 @@ namespace eval punk { } elseif {$isint || [regexp {^(end|end[-+]{1,2}[0-9]+)$} $index]} { if {$ptype eq "@"} { #compare position to *possibly updated* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index) - if {$isint} { + if {$isint} { append script [string map [list $listvar $index] { if {( > [llength $])} { - #not a pipesyntax error + #not a pipesyntax error error "pipedata insertionpattern index out of bounds. index: vs len: [llength $] use /x instead of @x to avoid check (list_insertion_script)" "list_insertion_script $keyspec" [list pipedata insertionpattern index_out_f_bounds] } }] } - #todo check end-x bounds? + #todo check end-x bounds? } if {$isint} { append script [string map [list $listvar $index $exp $data] { @@ -4455,98 +4483,20 @@ namespace eval punk { }] } - + } else { error "pipesyntax error in segment - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)4" "list_insertion_script $keyspec" [list pipesyntax insertionpattern_invalid] - } + } return $script } - #todo - consider whether we can use < for insertion/iteration combinations - # =a<,b< iterate once through - # =a><,b>< cartesian product - # =a<>,b<> ??? zip ? - # - # ie = {a b c} |> .=< inspect - # would call inspect 3 times, once for each argument - # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list - # would produce list of cartesian pairs? - # - proc _split_equalsrhs {insertionpattern} { - #map the insertionpattern so we can use faster globless info command search - set name_mapped [pipecmd_namemapping $insertionpattern] - set cmdname ::punk::pipecmds::split_rhs::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] - set i 0 - set return_triples [list] - foreach v_pos $lst_var_indexposition { - lassign $v_pos v index_and_position - #e.g varname@@data/ok>0 varname/1/0>end - #ensure only one ">" is detected - if {![string length $index_and_position]} { - set indexspec "" - set positionspec "" - } else { - set chars [split $index_and_position ""] - set posns [lsearch -all $chars ">"] - if {[llength $posns] > 1} { - error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - if {![llength $posns]} { - set indexspec $index_and_position - set positionspec "" - } else { - set splitposn [lindex $posns 0] - set indexspec [string range $index_and_position 0 $splitposn-1] - set positionspec [string range $index_and_position $splitposn+1 end] - } - } - - #review - - if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { - set star "" - if {$v eq "*"} { - set v "" - set star "*" - } - if {[string index $positionspec end] eq "*"} { - set star "*" - } - #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent - #as are /end and @end - #lset lst_var_indexposition $i [list $v "/end$star"] - set triple [list $v $indexspec "/end$star"] - } else { - if {$positionspec eq ""} { - #e.g just =varname - #lset lst_var_indexposition $i [list $v "/end"] - set triple [list $v $indexspec "/end"] - #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" - } else { - if {[string index $indexspec 0] ni [list "" "/" "@"]} { - error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - set triple [list $v $indexspec $positionspec] - } - } - lappend return_triples $triple - incr i - } - proc $cmdname {} [list return $return_triples] - return $return_triples - } - proc _is_math_func_prefix {e1} { #also catch starting brackets.. e.g "(min(4,$x) " if {[regexp {^[^[:alnum:]]*([[:alnum:]]*).*} $e1 _ word]} { - #possible math func + #possible math func if {$word in [info functions]} { return true } @@ -4583,8 +4533,8 @@ namespace eval punk { #puts "PERCENTS : $percents" set sequences [list] set in_sequence 0 - set start -1 - set end -1 + set start -1 + set end -1 set i 0 #todo - some more functional way of zipping/comparing these lists? set s_length 0 ;#sequence length including % symbols - minimum for tag therefore 2 @@ -4601,7 +4551,7 @@ namespace eval punk { } else { if {$n ^ $p} { incr s_length - incr end + incr end } else { if {$n & $p} { if {$s_length == 1} { @@ -4612,7 +4562,7 @@ namespace eval punk { set start $i set end $i } else { - incr end + incr end lappend sequences [list $start $end] set in_sequence 0 set s_length 0 @@ -4649,81 +4599,11 @@ namespace eval punk { return $output } - # - # - # relatively slow on even small sized scripts - proc arg_is_script_shaped2 {arg} { - set re {^(\s|;|\n)$} - set chars [split $arg ""] - if {[lsearch -regex $chars $re] >=0} { - return 1 - } else { - return 0 - } - } - - #exclude quoted whitespace - proc arg_is_script_shaped {arg} { - if {[tcl::string::first \n $arg] >= 0} { - return 1 - } elseif {[tcl::string::first ";" $arg] >= 0} { - return 1 - } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { - lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found - return [expr {$part2 ne ""}] - } else { - return 0 - } - } - proc _rhs_tail_split {fullrhs} { - set inq 0; set indq 0 - set equalsrhs "" - set i 0 - foreach ch [split $fullrhs ""] { - if {$inq} { - append equalsrhs $ch - if {$ch eq {'}} { - set inq 0 - } - } elseif {$indq} { - append equalsrhs $ch - if {$ch eq {"}} { - set indq 0 - } - } else { - switch -- $ch { - {'} { - set inq 1 - } - {"} { - set indq 1 - } - " " { - #whitespace outside of quoting - break - } - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} - default { - #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? - #we can't (reliably?) put \t as one of our switch keys - # - if {$ch eq "\t"} { - break - } - } - } - append equalsrhs $ch - } - incr i - } - set tail [tcl::string::range $fullrhs $i end] - return [list $equalsrhs $tail] - } # -- #consider possible tilde templating version ~= vs .= - #support ~ and ~* placeholders only. - #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* + #support ~ and ~* placeholders only. + #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* #The ~ being mapped to $data in the pipeline. #This is more readable and simpler for beginners - although it doesn't handle more advanced insertion requirements. #possibility to mix as we can already with .= and = @@ -4739,12 +4619,14 @@ namespace eval punk { #--------------------------------------------------------------------- # test if we have an initial x.=y.= or x.= y.= - + #nextail is tail for possible recursion based on first argument in the segment - set nexttail [lassign $fulltail next1] ;#tail head + #set nexttail [lassign $fulltail next1] ;#tail head + set next1 [lindex $args 0] switch -- $next1 { pipematch { + set nexttail [lrange $args 1 end] set results [uplevel 1 [list pipematch {*}$nexttail]] debug.punk.pipe {>>> pipematch results: $results} 1 @@ -4773,9 +4655,9 @@ namespace eval punk { #The second element is always treated as a raw value - not a pipeline instruction. #whereas... for execution: #.= x=y the second element is a pipeline-significant symbol based on the '=' even if it was passed in as an argument. - #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - + #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - #- but if the pipeline is designed to put an argument in the zero position - then presumably it is intended as a pipeline-significant element anyway - #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines + #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines # if {$segment_op ne "="} { #handle for example: @@ -4784,7 +4666,8 @@ namespace eval punk { #var1 will contain ETC (from entire pipeline), var2 will contain etc (from associated segment) # - if {([set nexteposn [string first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + if {([set nexteposn [string last = $next1]] >= 0) && (![punk::pipe::lib::arg_is_script_shaped $next1]) } { + set nexttail [lrange $args 1 end] #*SUB* pipeline recursion. #puts "======> recurse based on next1:$next1 " if {[string index $next1 $nexteposn-1] eq {.}} { @@ -4794,7 +4677,7 @@ namespace eval punk { #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5 #debug.punk.pipe {>>> results: $results} 1 return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]] - } + } #puts "======> recurse assign based on next1:$next1 " #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #} @@ -4819,17 +4702,17 @@ namespace eval punk { set more_pipe_segments 1 ;#first loop #this contains the main %data% and %datalist% values going forward in the pipeline - #as well as any extra pipeline vars defined in each |> + #as well as any extra pipeline vars defined in each |> #It also contains any 'args' with names supplied in <| set dict_tagval [dict create] ;#cumulative %x% tag dict which operates on the whole length of the pipeline #determine if there are input args at the end of the pipeline indicated by reverse <| symbol possibly with argspecs e.g transform x y z =0} { - set tailremaining [lrange $fulltail 0 $firstargpipe_posn-1] - set argslist [lrange $fulltail $firstargpipe_posn+1 end] ;#Note that this could be a whole other pipeline with |> and/or <| elements. - set argpipe [lindex $fulltail $firstargpipe_posn] - set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " and/or <| elements. + set argpipe [lindex $fulltail $firstargpipe_posn] + set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " 0}] #if {$segment_has_insertions} { # puts stdout ">>> $segment_members insertion_patterns $insertion_patterns" @@ -4994,7 +4877,7 @@ namespace eval punk { foreach {vname val} $pipedvars { #add additionally specified vars and allow overriding of %args% and %data% by not setting them here if {$vname eq "data"} { - #already potentially overridden + #already potentially overridden continue } dict set dict_tagval $vname $val @@ -5010,7 +4893,7 @@ namespace eval punk { #add previous_result as data in end position by default, only if *no* insertions specified (data is just list-wrapped previous_result) #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default as single element - not args - because some strings are not valid lists #insertion-specs with a trailing * can be used to insert data in args format - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { lappend segment_members_filled [dict get $dict_tagval data] } @@ -5020,7 +4903,7 @@ namespace eval punk { set segment_members_filled [list] set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign - set rhsmapped [pipecmd_namemapping $rhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $rhs] set cmdname "::punk::pipecmds::insertion::_$rhsmapped" #glob chars have been mapped - so we can test by comparing info commands result to empty string if {[info commands $cmdname] eq ""} { @@ -5057,13 +4940,14 @@ namespace eval punk { } if {[dict exists $dict_tagval $v]} { set insertion_data [dict get $dict_tagval $v] - #todo - use destructure_func + #todo - use destructure_func set d [punk::_multi_bind_result $indexspec $insertion_data] set insertion_data [punk::_handle_bind_result $d] } else { #review - skip error if varname is 'data' ? #e.g we shouldn't really fail for: #.=>* list a b c <| + #??? Technically #we need to be careful not to insert empty-list as an argument by default error "pipevariable - varname $v not present in pipeline context. pipecontext_vars: [dict keys $dict_tagval] (2)" " pipecontext_vars: [dict keys $dict_tagval]" [list pipevariable variable_not_in_pipeline_scope] } @@ -5098,9 +4982,9 @@ namespace eval punk { #set segment_members_filled $segmenttail #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) - + } - set rhs [string map $dict_tagval $rhs] ;#obsolete? + set rhs [string map $dict_tagval $rhs] ;#obsolete? debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 @@ -5109,8 +4993,8 @@ namespace eval punk { #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) if {(!$segment_first_is_script ) && $segment_op eq ".="} { - #no scriptiness detected - + #no scriptiness detected + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 set cmdlist_result [uplevel 1 $segment_members_filled] @@ -5119,25 +5003,25 @@ namespace eval punk { #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] - + set segment_result [_handle_bind_result $d] #puts stderr ">>forward_result: $forward_result segment_result $segment_result" } elseif {$segment_op eq "="} { - #slightly different semantics for assigment! - #We index into the DATA - not the position within the segment! + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! #(an = segment must take a single argument, as opposed to a .= segment) #(This was a deliberate design choice for consistency with set, and to reduce errors.) #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) # - #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data - #v= {a b c} |> = + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = # must return: {a b c} not a b c # if {!$segment_has_insertions} { - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { if {![llength $segment_members_filled]} { set segment_members_filled [dict get $dict_tagval data] @@ -5168,7 +5052,7 @@ namespace eval punk { lappend segmentargnames $k lappend segmentargvals $val } - + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" set add_argsdata 0 @@ -5255,7 +5139,7 @@ namespace eval punk { #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section #It may however make a good debug point #puts stderr "segment $i segment_result:$segment_result" - + debug.punk.pipe.rep {[rep_listname segment_result]} 3 @@ -5265,17 +5149,17 @@ namespace eval punk { #examine tailremaining. # either x x x |?> y y y ... # or just y y y - #we want the x side for next loop - + #we want the x side for next loop + #set up the conditions for the next loop - #|> x=y args + #|> x=y args # inpipespec - contents of previous piper |xxx> # outpipespec - empty or content of subsequent piper |xxx> - # previous_result + # previous_result # assignment (x=y) - set pipespec($j,in) $pipespec($i,out) + set pipespec($j,in) $pipespec($i,out) set outpipespec "" set tailmap "" set next_pipe_posn -1 @@ -5295,7 +5179,7 @@ namespace eval punk { if {[llength $tailremaining] || $next_pipe_posn >= 0} { if {$next_pipe_posn >=0} { - set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] } else { @@ -5311,7 +5195,7 @@ namespace eval punk { set rhs "" set segment_first_is_script 0 if {[llength $next_all_members]} { - if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + if {[punk::pipe::lib::arg_is_script_shaped [lindex $next_all_members 0]]} { set segment_first_word [lindex $next_all_members 0] set segment_first_is_script 1 set segment_op "" @@ -5322,7 +5206,7 @@ namespace eval punk { if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op ".=" set segment_first_word [lindex $next_all_members 1] - set script_like_first_word [arg_is_script_shaped $segment_first_word] + set script_like_first_word [punk::pipe::lib::arg_is_script_shaped $segment_first_word] if {$script_like_first_word} { set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= } @@ -5330,7 +5214,7 @@ namespace eval punk { } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op "=" #never scripts - #must be at most a single element after the = ! + #must be at most a single element after the = ! if {[llength $next_all_members] > 2} { #raise this as pipesyntax as opposed to pipedata? error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] @@ -5341,7 +5225,7 @@ namespace eval punk { } else { set segment_is_list 1 ;#only used for segment_op = } - + set segment_members $segment_first_word } else { #no assignment operator and not script shaped @@ -5357,7 +5241,7 @@ namespace eval punk { } else { #?? two pipes in a row ? - debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 set segment_members return set segment_first_word return } @@ -5369,7 +5253,7 @@ namespace eval punk { } else { debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 #output pipe spec at tail of pipeline - + set pipedvars [dict create] if {[string length $pipespec($i,out)]} { set d [apply {{mv res} { @@ -5382,7 +5266,7 @@ namespace eval punk { set more_pipe_segments 0 } - #the segment_result is based on the leftmost var on the lhs of the .= + #the segment_result is based on the leftmost var on the lhs of the .= #whereas forward_result is always the entire output of the segment #JMN2 #lappend segment_result_list [join $segment_result] @@ -5414,7 +5298,7 @@ namespace eval punk { } set s $posn } else { - #int + #int if {($start < 0) || ($start > ($datalen -1))} { return 0 } @@ -5430,7 +5314,7 @@ namespace eval punk { } set e $posn } else { - #int + #int if {($end < 0)} { return 0 } @@ -5448,7 +5332,7 @@ namespace eval punk { if {$e < $s} { return 0 } - + return [expr {$e - $s + 1}] } @@ -5601,11 +5485,11 @@ namespace eval punk { #windows experiment todo - use twapi and named pipes - #twapi::namedpipe_server {\\.\pipe\something} + #twapi::namedpipe_server {\\.\pipe\something} #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc # - + if {[string first " " $new] > 0} { set c1 $name } else { @@ -5619,8 +5503,8 @@ namespace eval punk { #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] - - if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + + if {[punk::config::configure running auto_exec_mechanism] eq "experimental"} { #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it #not a trivial task @@ -5628,16 +5512,16 @@ namespace eval punk { #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output #ctrl-c propagation also needs to be considered - set teehandle punksh + set teehandle punksh uplevel 1 [list ::catch \ [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ ::tcl::UnknownResult ::tcl::UnknownOptions] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { - dict set ::tcl::UnknownOptions -code error + dict set ::tcl::UnknownOptions -code error set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" } else { - #no point returning "exitcode 0" if that's the only non-error return. + #no point returning "exitcode 0" if that's the only non-error return. #It is misleading. Better to return empty string. set ::tcl::UnknownResult "" } @@ -5647,10 +5531,10 @@ namespace eval punk { set redir ">&@stdout <@stdin" uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] - #we can't detect stdout/stderr output from the exec - #for now emit an extra \n on stderr + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr #todo - there is probably no way around this but to somehow exec in the context of a completely separate console - #This is probably a tricky problem - especially to do cross-platform + #This is probably a tricky problem - especially to do cross-platform # # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit if {[dict get $::tcl::UnknownOptions -code] == 0} { @@ -5747,7 +5631,7 @@ namespace eval punk { } } - + } return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" @@ -5756,11 +5640,12 @@ namespace eval punk { proc know {cond body} { set existing [info body ::unknown] #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) - ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##This means we can't have 2 different conds with same body if we test for body in unknown. ##if {$body ni $existing} { - package require base64 set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + + #tclint-disable-next-line proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { #--------------------------------------- if {![catch {expr {@c@}} res] && $res} { @@ -5779,7 +5664,6 @@ namespace eval punk { } proc decodescript {b64} { if {[ catch { - package require base64 base64::decode $b64 } scr]} { return "" @@ -5817,36 +5701,36 @@ namespace eval punk { if {[info commands ::tsv::set] eq ""} { puts stderr "set_repl_last_unknown - tsv unavailable!" return - } + } tsv::set repl last_unknown {*}$args } # --------------------------- + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- proc configure_unknown {} { #----------------------------- #these are critical e.g core behaviour or important for repl displaying output correctly - - #---------------- - #for var="val {a b c}" - #proc ::punk::val {{v {}}} {tailcall lindex $v} - #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version - proc ::punk::val [list [list v [purelist]]] {return $v} - #---------------- + #can't use know - because we don't want to return before original unknown body is called. proc ::unknown {args} [string cat { - package require base64 #set ::punk::last_run_display [list] #set ::repl::last_unknown [lindex $args 0] ;#jn #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW - punk::set_repl_last_unknown [lindex $args 0] + punk::set_repl_last_unknown [lindex $args 0] }][info body ::unknown] #handle process return dict of form {exitcode num etc blah} #ie when the return result as a whole is treated as a command - #exitcode must be the first key + #exitcode must be the first key know {[lindex $args 0 0] eq "exitcode"} { uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] } @@ -5854,13 +5738,13 @@ namespace eval punk { #----------------------------- # - # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. - + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + #todo - repl output info that it was evaluated as an expression #know {[expr $args] || 1} {expr $args} know {[expr $args] || 1} {tailcall expr $args} - #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} @@ -5879,18 +5763,18 @@ namespace eval punk { error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs tail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs tail } #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah # we only look at leftmost namespace-like thing and need to take account of the pattern syntax - # e.g for ::etc,'::x'= + # e.g for ::etc,'::x'= # the ns is :: and the tail is etc,'::x'= # (Tcl's namespace qualifiers/tail won't help here) if {[string match ::* $hd]} { - set patterns [punk::_split_patterns_memoized $hd] + set patterns [punk::pipe::lib::_split_patterns_memoized $hd] #get a pair-list something like: {::x /0} {etc {}} set ns [namespace qualifiers [lindex $patterns 0 0]] set nslen [string length $ns] @@ -5904,27 +5788,27 @@ namespace eval punk { } else { set nscaller [uplevel 1 [list ::namespace current]] #jmn - set rhsmapped [pipecmd_namemapping $equalsrhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $equalsrhs] set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk #we must check for exact match of the command in the list - because command could have glob chars. if {"$pattern=$rhsmapped" in $commands} { puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" #we call the namespaced function - we don't evaluate it *in* the namespace. #REVIEW - #warn for now...? + #warn for now...? #tailcall $pattern=$equalsrhs {*}$args tailcall $pattern=$rhsmapped {*}$tail } } #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" - #ignore the namespace.. + #ignore the namespace.. #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] } - #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list #e.g x=a\nb c @@ -5992,12 +5876,12 @@ namespace eval punk { error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs argstail } #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail @@ -6018,8 +5902,8 @@ namespace eval punk { know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} #add escaping backslashes to a value - #matching odd keys in dicts using pipeline syntax can be tricky - as - #e.g + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g #set ktest {a"b} #@@[escv $ktest].= list a"b val #without escv: @@ -6033,14 +5917,14 @@ namespace eval punk { #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically #thanks to DKF regsub -all {\W} $v {\\&} - } + } interp alias {} escv {} punk::escv #review #set v "\u2767" # #escv $v #\ - #the + #the #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { @@ -6048,17 +5932,17 @@ namespace eval punk { # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! # #avoid using the return from expr and it works: # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } - # + # # tailcall ::punk::match_exec $varspecs $rhs {*}$tail # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] #} } - configure_unknown + configure_unknown #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. # - #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. proc % {args} { set arglist [lassign $args assign] ;#tail, head @@ -6068,12 +5952,12 @@ namespace eval punk { tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] } - set is_script [punk::arg_is_script_shaped $assign] + set is_script [punk::pipe::lib::arg_is_script_shaped $assign] if {!$is_script && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6092,7 +5976,7 @@ namespace eval punk { tailcall {*}$cmdlist - #result-based mismatch detection can probably never work nicely.. + #result-based mismatch detection can probably never work nicely.. #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! # set result [uplevel 1 $cmdlist] @@ -6128,10 +6012,10 @@ namespace eval punk { set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] } elseif {$assign eq "="} { set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} # set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} # set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6143,10 +6027,10 @@ namespace eval punk { } } else { set cmdlist $args - #script? + #script? #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 $cmdlist} result erroptions]} { #puts stderr "pipematch erroptions:$erroptions" #debug.punk.pipe {pipematch error $result} 4 @@ -6236,7 +6120,7 @@ namespace eval punk { } } - #should only raise an error for pipe syntax errors - all other errors should be wrapped + #should only raise an error for pipe syntax errors - all other errors should be wrapped proc pipecase {args} { #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 set arglist [lassign $args assign] @@ -6245,10 +6129,10 @@ namespace eval punk { } elseif {$assign eq "="} { #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] set cmdlist [list ::= {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { @@ -6257,15 +6141,15 @@ namespace eval punk { set cmdlist [list $assign {*}$arglist] #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] } else { - error "pipesyntax pipecase unable to interpret pipeline '$args'" + error "pipesyntax pipecase unable to interpret pipeline '$args'" } #todo - account for insertion-specs e.g x=* x.=/0* } else { - #script? + #script? set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { #puts stderr "====>>> result: $result erroptions" set ecode [dict get $erroptions -errorcode] @@ -6308,14 +6192,14 @@ namespace eval punk { return [dict create error [dict create suppressed $result]] } default { - #normal tcl error + #normal tcl error #return [dict create error [dict create reason $result]] tailcall error $result "pipecase $args" [list caseerror] } } } } else { - tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] } } @@ -6329,7 +6213,7 @@ namespace eval punk { #unset args #upvar args upargs #set upargs $nextargs - upvar switchargs switchargs + upvar switchargs switchargs set switchargs $args uplevel 1 [::list ::if 1 $pipescript] } @@ -6339,7 +6223,7 @@ namespace eval punk { proc pipeswitchc {pipescript args} { set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list info vars] } else { set get_vars [list info locals] @@ -6377,13 +6261,13 @@ namespace eval punk { % - pipematch - ispipematch { incr i set e2 [lindex $args $i] - #set body [list $e {*}$e2] + #set body [list $e {*}$e2] #append body { $data} - - set body [list $e {*}$e2] + + set body [list $e {*}$e2] append body { {*}$data} - - + + set applylist [list {data} $body] #puts stderr $applylist set r [apply $applylist $r] @@ -6393,7 +6277,7 @@ namespace eval punk { incr i set e2 [lindex $args $i] set body [list $e $e2] - #pipeswitch takes 'args' - so expand $data when in pipedata context + #pipeswitch takes 'args' - so expand $data when in pipedata context append body { {*}$data} #use applylist instead of uplevel when in pipedata context! #can use either switchdata/data but not vars in calling context of 'pipedata' command. @@ -6421,8 +6305,7 @@ namespace eval punk { proc scriptlibpath {{shortname {}} args} { - upvar ::punk::config::running running_config - set scriptlib [dict get $running_config scriptlib] + set scriptlib [punk::config::configure running scriptlib] if {[string match "lib::*" $shortname]} { set relpath [string map [list "lib::" "" "::" "/"] $shortname] set relpath [string trimleft $relpath "/"] @@ -6452,7 +6335,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { set sep ";" } else { - # : ok for linux/bsd ... mac? + # : ok for linux/bsd ... mac? set sep ":" } set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] @@ -6465,7 +6348,7 @@ namespace eval punk { } proc path {{glob *}} { set pipe [punk::path_list_pipe $glob] - {*}$pipe |> list_as_lines + {*}$pipe |> list_as_lines } #------------------------------------------------------------------- @@ -6508,7 +6391,7 @@ namespace eval punk { #e.g unix files such as /dev/null vs windows devices such as CON,PRN #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! - #We will stick with the Tcl view of the file system. + #We will stick with the Tcl view of the file system. #User can use their own direct calls to external utils if #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] proc sh_TEST {args} { @@ -6526,7 +6409,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { #e.g trailing dot or trailing space if {[punk::winpath::illegalname_test $a2]} { - #protect with \\?\ to stop windows api from parsing + #protect with \\?\ to stop windows api from parsing #will do nothing if already prefixed with \\?\ set a2 [punk::winpath::illegalname_fix $a2] @@ -6536,7 +6419,7 @@ namespace eval punk { switch -- $a1 { -b { #dubious utility on FreeBSD, windows? - #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' #Linux apparently uses them though if{[file exists $a2]} { set boolresult [expr {[file type $a2] eq "blockSpecial"}] @@ -6545,7 +6428,7 @@ namespace eval punk { } } -c { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { set boolresult [expr {[file type $a2] eq "characterSpecial"}] } else { @@ -6559,9 +6442,9 @@ namespace eval punk { set boolresult [file exists $a2] } -f { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { - set boolresult [expr {[file type $a2] eq "file"}] + set boolresult [expr {[file type $a2] eq "file"}] } else { set boolresult false } @@ -6621,7 +6504,7 @@ namespace eval punk { } "-eq" { #test expects a possibly-large integer-like thing - #shell scripts will + #shell scripts will if {![is_sh_test_integer $a1]} { puts stderr "sh_TEST: invalid integer '$a1'" set lasterr 2 @@ -6725,7 +6608,7 @@ namespace eval punk { set exitcode [dict get $callinfo exitcode] if {[string length $errinfo]} { puts stderr "sh_TEST error in external call to 'test $args': $errinfo" - set lasterr $exitcode + set lasterr $exitcode } if {$exitcode == 0} { set boolresult true @@ -6761,7 +6644,7 @@ namespace eval punk { set c [lindex $args 0] if {[string is integer -strict $c]} { #return [expr {$c == 0}] - #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true if {$c == 0} { return true } else { @@ -6801,7 +6684,7 @@ namespace eval punk { #maint - punk::args has similar #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions - #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? #JMN #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. @@ -6857,7 +6740,7 @@ namespace eval punk { foreach {k v} $rawargs { if {![string match -* $k]} { break - } + } if {$i+1 >= [llength $rawargs]} { #no value for last flag error "bad options for $caller. No value supplied for last option $k" @@ -6957,7 +6840,7 @@ namespace eval punk { #NOT attempting to match haskell other than in overall concept. # - #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. # @@ -7046,7 +6929,7 @@ namespace eval punk { } #group_numlist ? preserve representation of numbers rather than use string comparison? - + # - group_string #.= punk::group_string "aabcccdefff" @@ -7131,7 +7014,7 @@ namespace eval punk { #review #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? #Perhaps will be solved by: Tip 550: Garbage collection for TclOO - #Theoretically this should allow tidy up of objects created within the pipeline automatically + #Theoretically this should allow tidy up of objects created within the pipeline automatically #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. proc matrix_command_from_rows {matrix_rows} { set mcmd [struct::matrix] @@ -7147,7 +7030,7 @@ namespace eval punk { set filtered_list [list] set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list ::info vars] } else { set get_vars [list ::info locals] @@ -7227,38 +7110,89 @@ namespace eval punk { return $linelist } - - #An implementation of a notoriously controversial metric. - proc LOC {args} { - set argspecs [subst { + namespace eval argdoc { + set DYN_ANTIGLOB_PATHS {${[punk::args::resolved_def -types opts ::punk::path::treefilenames -antiglob_paths]}} + punk::args::define { + @dynamic + @id -id ::punk::LOC + @cmd -name punk::LOC\ + -summary\ + "Lines Of Code counter"\ + -help\ + "LOC - lines of code. + An implementation of a notoriously controversial metric. + Returns a dict or dictionary-display containing various + counts such as: + 'loc' - total lines of code. + 'purepunctuationlines' - lines consisting soley of punctuation. + 'filecount' - number of files examined." + @opts + -return -default showdict -choices {dict showdict} -dir -default "\uFFFF" -exclude_dupfiles -default 1 -type boolean + ${$DYN_ANTIGLOB_PATHS} + -antiglob_files -default "" -type list -help\ + "Exclude if file tail matches any of these patterns" -exclude_punctlines -default 1 -type boolean + -show_largest -default 0 -type integer -help\ + "Report the top largest linecount files. + The value represents the number of files + to report on." + } " + #we could map away whitespace and use string is punct - but not as flexible? review -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } - }] - set argd [punk::args::get_dict $argspecs $args] - lassign [dict values $argd] leaders opts vals - set searchspecs [dict values $vals] + " { + @values + fileglob -type string -default * -optional 1 -multiple 1 -help\ + "glob patterns to match against the filename portion (last segment) of each + file path. e.g *.tcl *.tm" + } + } + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argd [punk::args::parse $args withid ::punk::LOC] + lassign [dict values $argd] leaders opts values received + set searchspecs [dict get $values fileglob] - # -- --- --- --- --- --- - set opt_dir [dict get $opts -dir] + # -- --- --- --- --- --- + set opt_return [dict get $opts -return] + set opt_dir [dict get $opts -dir] if {$opt_dir eq "\uFFFF"} { set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list } - # -- --- --- --- --- --- - set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] - set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars - set opt_punctchars [dict get $opts -punctchars] - # -- --- --- --- --- --- + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + set opt_largest [dict get $opts -show_largest] + set opt_antiglob_paths [dict get $opts -antiglob_paths] + set opt_antiglob_files [dict get $opts -antiglob_files] + # -- --- --- --- --- --- - set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set filepaths [punk::path::treefilenames -dir $opt_dir -antiglob_paths $opt_antiglob_paths -antiglob_files $opt_antiglob_files {*}$searchspecs] set loc 0 - set dupfileloc 0 - set seentails [list] + set dupfileloc 0 + set seentails [dict create] + set seencksums [dict create] ;#key is cksum value is list of paths + set largestloc [dict create] set dupfilecount 0 - set extensions [list] + set extensions [list] set purepunctlines 0 + set dupinfo [dict create] + set has_hashfunc [expr {![catch {package require sha1}]}] + set notes "" + if {$has_hashfunc} { + set dupfilemech sha1 + if {$opt_exclude_punctlines} { + append notes "checksums are on content stripped of whitespace lines,trailing whitespace, and pure punct lines. Does not indicate file contents equal.\n" + } else { + append notes "checksums are on content stripped of whitespace lines and trailing whitespace. Does not indicate file contents equal.\n" + } + } else { + set dupfilemech filetail + append notes "dupfilemech filetail because sha1 not loadable\n" + } foreach fpath $filepaths { set isdupfile 0 set floc 0 @@ -7267,111 +7201,318 @@ namespace eval punk { if {$ext ni $extensions} { lappend extensions $ext } + if {[catch {fcat $fpath} contents]} { + puts stderr "Error processing $fpath\n $contents" + continue + } + set lines [linelist -line {trimright} -block {trimall} $contents] if {!$opt_exclude_punctlines} { - set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + set floc [llength $lines] + set comparedlines $lines } else { - set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] set mapawaypunctuation [list] foreach p $opt_punctchars empty {} { lappend mapawaypunctuation $p $empty } + set comparedlines [list] foreach ln $lines { if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { incr floc + lappend comparedlines $ln } else { incr fpurepunctlines - } + } } } - if {[file tail $fpath] in $seentails} { - set isdupfile 1 - incr dupfilecount - incr dupfileloc $floc + if {$opt_largest > 0} { + dict set largestloc $fpath $floc + } + if {$has_hashfunc} { + set cksum [sha1::sha1 [encoding convertto utf-8 [join $comparedlines \n]]] + if {[dict exists $seencksums $cksum]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + dict lappend seencksums $cksum $fpath + } else { + dict set seencksums $cksum [list $fpath] + } + } else { + if {[dict exists $seentails [file tail $fpath]]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } } if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { incr loc $floc incr purepunctlines $fpurepunctlines } - lappend seentails [file tail $fpath] + dict lappend seentails [file tail $fpath] $fpath + #lappend seentails [file tail $fpath] + } + if {$has_hashfunc} { + dict for {cksum paths} $seencksums { + if {[llength $paths] > 1} { + dict set dupinfo checksums $cksum $paths + } + } + } + dict for {tail paths} $seentails { + if {[llength $paths] > 1} { + dict set dupinfo sametail $tail $paths + } } + if {$opt_exclude_punctlines} { - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + purepunctuationlines $purepunctlines\ + notes $notes\ + ] + } else { + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + notes $notes\ + ] + } + if {$opt_largest > 0} { + set largest_n [dict create] + set sorted [lsort -stride 2 -index 1 -decreasing -integer $largestloc] + set kidx 0 + for {set i 0} {$i < $opt_largest} {incr i} { + if {$kidx+1 > [llength $sorted]} {break} + dict set largest_n [lindex $sorted $kidx] [lindex $sorted $kidx+1] + incr kidx 2 + } + dict set result largest $largest_n + } + if {$opt_return eq "showdict"} { + return [punk::lib::showdict $result @@dupinfo/*/* !@@dupinfo] } - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + return $result } + ##dict of lists? + #a + # 1 + # 2 + #b + # 3 + # 4 + # "" + # etc + # d + # D + # "ok then" + + + ##dict of dicts + #a + # x + # 1 + # y + # 2 + #b + # x + # 11 + + ##dict of mixed + #list + # a + # b + # c + #dict + # a + # aa + # b + # bb + #val + # x + #list + # a + # b + + # each line has 1 key or value OR part of 1 key or value. ie <=1 key/val per line! + ##multiline + #key + # "multi + # line value" + # + #-------------------------------- + #a + # 1 + # 2 + + #vs + + #a + # 1 + # 2 + + #dict of list-len 2 is equiv to dict of dict with one keyval pair + #-------------------------------- + + - #!!!todo fix - linedict is unfinished and non-functioning - #linedict based on indents + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents proc linedict {args} { + puts stderr "linedict is experimental and incomplete" set data [lindex $args 0] - set opts [lrange $args 1 end] ;#todo + set opts [lrange $args 1 end] ;#todo set nlsplit [split $data \n] set rootindent -1 set stepindent -1 - #set wordlike_parts [regexp -inline -all {\S+} $lastitem] - set d [dict create] - set keys [list] - set i 1 - set firstkeyline "N/A" - set firststepline "N/A" + + #first do a partial loop through lines and work out the rootindent and stepindent. + #we could do this in the main loop - but we do it here to remove a small bit of logic from the main loop. + #review - if we ever move to streaming a linedict - we'll need to re-arrange to validating indents as we go anyway. + set linenum 0 + set firstkey_line "N/A" + set firstkey_linenum -1 + set firststep_line "N/A" + set firststep_linenum -1 + set indents_seen [dict create] foreach ln $nlsplit { + incr linenum if {![string length [string trim $ln]]} { - incr i continue } - set is_rootkey 0 + + #todo - use info complete to accept keys/values with newlines regexp {(\s*)(.*)} $ln _ space linedata - puts stderr ">>line:'$ln' [string length $space] $linedata" - set this_indent [string length $space] - if {$rootindent < 0} { - set firstkeyline $ln - set rootindent $this_indent + if {[catch {lindex $linedata 0}]} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent == $rootindent} { - set is_rootkey 1 + if {[llength $linedata] > 1} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent < $rootindent} { - error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + #puts stderr "--linenum:[format %-3s $linenum] line:[format "%-20s" $ln] [format %-4s [string length $space]] $linedata" + set this_indent [string length $space] + if {[dict exists $indents_seen $this_indent]} { + continue } - if {$is_rootkey} { - dict set d $linedata {} - lappend keys $linedata + if {$rootindent < 0} { + set firstkey_line $ln + set firstkey_linenum $linenum + set rootindent $this_indent + dict set indents_seen $this_indent 1 + } elseif {$stepindent < 0} { + if {$this_indent > $rootindent} { + set firststep_line $ln + set firststep_linenum $linenum + set stepindent [expr {$this_indent - $rootindent}] + dict set indents_seen $this_indent 1 + } elseif {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" + } + #if equal - it's just another root key } else { - if {$stepindent < 0} { - set stepindent $this_indent - set firststepline $ln + #validate all others + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" } - if {$this_indent == $stepindent} { - dict set d [lindex $keys end] $ln + if {($this_indent - $rootindent) % $stepindent != 0} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. this_indent - rootindent ($this_indent - $rootindent == [expr {$this_indent - $rootindent}]) is not a multiple of the first key indent $stepindent seen on linenumber: $firststep_linenum value:'$firststep_line'" } else { - if {($this_indent % $stepindent) != 0} { - error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" - } + dict set indents_seen $this_indent 1 + } + } + } + - #todo fix! + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set linenum 0 ;#line-numbers 1 based + foreach ln $nlsplit { + incr linenum + if {![string length [string trim $ln]]} { + incr linenum + continue + } + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>linenum:[format %-3s $linenum] line:[format "%-20s " $ln] [format %-4s [string length $space]] $linedata" + set linedata [lindex $linedata 0] + set this_indent [string length $space] + + + if {$this_indent == $rootindent} { + #is rootkey + dict set d $linedata {} + set keys [list $linedata] + } else { + set ispan [expr {$this_indent - $rootindent}] + set numsteps [expr {$ispan / $stepindent}] + #assert - since validated in initial loop - numsteps is always >= 1 + set keydepth [llength $keys] + if {$numsteps > $keydepth + 1} { + #too deep - not tested for in initial loop. ? todo - convert to leading spaces in key/val? + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + if {$numsteps > ($keydepth - 1)} { + #assert - from above test - must be 1 or 2 deeper set parentkey [lindex $keys end] - lappend keys [list $parentkey $ln] - set oldval [dict get $d $parentkey] - if {[string length $oldval]} { - set new [dict create $oldval $ln] + set oldval [dict get $d {*}$parentkey] + if {$numsteps - ($keydepth -1) == 1} { + #1 deeper + if {$oldval ne {}} { + lappend keys [list {*}$parentkey $linedata] + dict unset d {*}$parentkey + #dict set d {*}$parentkey $oldval $linedata + dict set d {*}$parentkey $oldval {} ;#convert to key? + dict set d {*}$parentkey $linedata {} + } else { + dict set d {*}$parentkey $linedata + } } else { - dict set d $parentkey $ln - } - + #2 deeper - only ok if there is an existing val + if {$oldval eq {}} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + puts ">>> 2deep d:'$d' oldval:$oldval linedata:$linedata parentkey:$parentkey" + dict unset d {*}$parentkey + dict set d {*}$parentkey $oldval $linedata + lappend keys [list {*}$parentkey $oldval] + } + } elseif {$numsteps < ($keydepth - 1)} { + set diff [expr {$keydepth - 1 - $numsteps}] + set keys [lrange $keys 0 end-$diff] + #now treat as same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} + } else { + #same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} } } - incr i + #puts ">>keys:$keys" } return $d } - proc dictline {d} { + proc dictline {d {indent 2}} { puts stderr "unimplemented" set lines [list] - + return $lines } @@ -7414,79 +7555,79 @@ namespace eval punk { @id -id ::punk::inspect @cmd -name punk::inspect -help\ "Function to display values - used pimarily in a punk pipeline. - The raw value arguments (not options) are always returned to pass - forward in the pipeline. - (pipeline data inserted at end of each |...> segment is passed as single item unless - inserted with an expanding insertion specifier such as .=>* ) - e.g1: - .= list a b c |v1,/1-end,/0>\\ - .=>* inspect -label i1 -- |>\\ - .=v1> inspect -label i2 -- |>\\ - string toupper - (3) i1: {a b c} {b c} a - (1) i2: a b c - - - A B C - " + The raw value arguments (not options) are always returned to pass + forward in the pipeline. + (pipeline data inserted at end of each |...> segment is passed as single item unless + inserted with an expanding insertion specifier such as .=>* ) + e.g1: + .= list a b c |v1,/1-end,/0>\\ + .=>* inspect -label i1 -- |>\\ + .=v1> inspect -label i2 -- |>\\ + string toupper + (3) i1: {a b c} {b c} a + (1) i2: a b c + + - A B C + " -label -type string -default "" -help\ "An optional label to help distinguish output when multiple - inspect statements are in a pipeline. This appears after the - bracketed count indicating number of values supplied. - e.g (2) MYLABEL: val1 val2 - The label can include ANSI codes. - e.g - inspect -label [a+ red]mylabel -- val1 val2 val3 - " + inspect statements are in a pipeline. This appears after the + bracketed count indicating number of values supplied. + e.g (2) MYLABEL: val1 val2 + The label can include ANSI codes. + e.g + inspect -label [a+ red]mylabel -- val1 val2 val3 + " -limit -type int -default 20 -help\ "When multiple values are passed to inspect - limit the number - of elements displayed in -channel output. - When truncation has occured an elipsis indication (...) will be appended. - e.g - .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ - (11) 20 23 26 29... + of elements displayed in -channel output. + When truncation has occured an elipsis indication (...) will be appended. + e.g + .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ + (11) 20 23 26 29... - - 385 + - 385 - For no limit - use -limit -1 - " + For no limit - use -limit -1 + " -channel -type string -default stderr -help\ "An existing open channel to write to. If value is any of nul, null, /dev/nul - the channel output is disabled. This effectively disables inspect as the args - are simply passed through in the return to continue the pipeline. - " + the channel output is disabled. This effectively disables inspect as the args + are simply passed through in the return to continue the pipeline. + " -showcount -type boolean -default 1 -help\ "Display a leading indicator in brackets showing the number of arg values present." -ansi -type integer -default 1 -nocase 1 -choices {0 1 2 VIEW 3 VIEWCODES 4 VIEWSTYLES} -choicelabels { 0 "Strip ANSI codes from display - of values. The disply output will - still be colourised if -ansibase has - not been set to empty string or - [a+ normal]. The stderr or stdout - channels may also have an ansi colour. - (see 'colour off' or punk::config)" + of values. The disply output will + still be colourised if -ansibase has + not been set to empty string or + [a+ normal]. The stderr or stdout + channels may also have an ansi colour. + (see 'colour off' or punk::config)" 1 "Leave value as is" 2 "Display the ANSI codes and - other control characters inline - with replacement indicators. - e.g esc, newline, space, tab" + other control characters inline + with replacement indicators. + e.g esc, newline, space, tab" VIEW "Alias for 2" - 3 "Display as per 2 but with - colourised ANSI replacement codes." + 3 "Display as per 2 but with + colourised ANSI replacement codes." VIEWCODES "Alias for 3" 4 "Display ANSI and control - chars in default colour, but - apply the contained ansi to - the text portions so they display - as they would for -ansi 1" - VIEWSTYLE "Alias for 4" - } + chars in default colour, but + apply the contained ansi to + the text portions so they display + as they would for -ansi 1" + VIEWSTYLE "Alias for 4" + } -ansibase -type ansistring -default {${[a+ brightgreen]}} -help\ "Base ansi code(s) that will apply to output written to the chosen -channel. - If there are ansi resets in the displayed values - output will revert to this base. - Does not affect return value." + If there are ansi resets in the displayed values - output will revert to this base. + Does not affect return value." -- -type none -help\ "End of options marker. - It is advisable to use this, as data in a pipeline may often begin with -" + It is advisable to use this, as data in a pipeline may often begin with -" @values -min 0 -max -1 arg -type string -optional 1 -multiple 1 -help\ @@ -7500,7 +7641,7 @@ namespace eval punk { set flags [list] set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- if {$endoptsposn >= 0} { - set flags [lrange $args 0 $endoptsposn-1] + set flags [lrange $args 0 $endoptsposn-1] set pipeargs [lrange $args $endoptsposn+1 end] } else { #no explicit end of opts marker @@ -7551,7 +7692,7 @@ namespace eval punk { set val [lindex $pipeargs 0] set count 1 } else { - #but the pipeline segment could have an insertion-pattern ending in * + #but the pipeline segment could have an insertion-pattern ending in * set val $pipeargs set count [llength $pipeargs] } @@ -7597,7 +7738,7 @@ namespace eval punk { set ansibase [dict get $opts -ansibase] if {$ansibase ne ""} { - #-ansibase default is hardcoded into punk::args definition + #-ansibase default is hardcoded into punk::args definition #run a test using any ansi code to see if colour is still enabled if {[a+ red] eq ""} { set ansibase "" ;#colour seems to be disabled @@ -7609,27 +7750,31 @@ namespace eval punk { set displayval $ansibase[punk::ansi::ansistrip $displayval] } 1 { - #val may have ansi - including resets. Pass through ansibase_lines to + #val may have ansi - including resets. Pass through ansibase_lines to if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 2 { set displayval $ansibase[ansistring VIEW $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 3 { set displayval $ansibase[ansistring VIEWCODE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 4 { set displayval $ansibase[ansistring VIEWSTYLE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } } @@ -7665,6 +7810,7 @@ namespace eval punk { set cmdinfo [list] lappend cmdinfo [list help "?topics?" "This help. To see available subitems type: help topics"] lappend cmdinfo [list i "cmd ?subcommand...?" "Show usage for a command or ensemble subcommand"] + lappend cmdinfo [list s "cmd ?subcommand...?" "Show synopsis for a command or ensemble subcommand"] lappend cmdinfo [list ./ "?subdir?" "view/change directory"] lappend cmdinfo [list ../ "" "go up one directory"] lappend cmdinfo [list ./new "subdir" "make new directory and switch to it"] @@ -7692,9 +7838,9 @@ namespace eval punk { $t configure_column 1 -minwidth [expr {$width_1 + 1}] $t configure -title $title - set text "" + set text "" append text [$t print] - + set warningblock "" set introblock $mascotblock @@ -7743,14 +7889,14 @@ namespace eval punk { upvar ::punk::config::other_env_vars_config otherenv_config set known_punk [dict keys $punkenv_config] - set known_other [dict keys $otherenv_config] + set known_other [dict keys $otherenv_config] append text \n set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. - #The Tcl ::env array is linked to the underlying process view of the environment + #The Tcl ::env array is linked to the underlying process view of the environment #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. #an 'array get' will resynchronise. #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. @@ -7759,7 +7905,7 @@ namespace eval punk { #do an array read on ::env foreach {v vinfo} $punkenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7778,7 +7924,7 @@ namespace eval punk { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7795,12 +7941,12 @@ namespace eval punk { append text $linesep\n append text "punk environment vars:\n" append text $linesep\n - set col1 [string repeat " " 25] + set col1 [string repeat " " 25] set col2 [string repeat " " 50] foreach v $known_punk { set c1 [overtype::left $col1 $v] if {[info exists ::env($v)]} { - set c2 [overtype::left $col2 [set ::env($v)] + set c2 [overtype::left $col2 [set ::env($v)]] } else { set c2 [overtype::right $col2 "(NOT SET)"] } @@ -7816,27 +7962,33 @@ namespace eval punk { set indent [string repeat " " [string length "WARNING: "]] lappend cstring_tests [dict create\ type "PM "\ - msg "PRIVACY MESSAGE"\ + msg "UN"\ f7 punk::ansi::controlstring_PM\ - f7desc "7bit ESC ^"\ + f7prefix "7bit ESC ^ secret "\ + f7suffix "safe"\ f8 punk::ansi::controlstring_PM8\ - f8desc "8bit \\x9e"\ + f8prefix "8bit \\x9e secret "\ + f8suffix "safe"\ ] lappend cstring_tests [dict create\ type SOS\ - msg "STRING"\ + msg "NOT"\ f7 punk::ansi::controlstring_SOS\ - f7desc "7bit ESC X"\ + f7prefix "7bit ESC X string "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_SOS8\ - f8desc "8bit \\x98"\ + f8prefix "8bit \\x98 string "\ + f8suffix " hidden"\ ] lappend cstring_tests [dict create\ type APC\ - msg "APPLICATION PROGRAM COMMAND"\ + msg "NOT"\ f7 punk::ansi::controlstring_APC\ - f7desc "7bit ESC _"\ + f7prefix "7bit ESC _ APPLICATION PROGRAM COMMAND "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_APC8\ - f8desc "8bit \\x9f"\ + f8prefix "8bit \\x9f APPLICATION PROGRAM COMMAND "\ + f8suffix " hidden"\ ] foreach test $cstring_tests { @@ -7846,14 +7998,14 @@ namespace eval punk { set hidden_width_m8 [punk::console::test_char_width $m8] if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { if {$hidden_width_m == 0} { - set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + set d "[a+ green bold][dict get $test f7prefix][a red]${m}[a][a+ green bold][dict get $test f7suffix][a]" } else { - set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + set d "[a+ yellow bold][dict get $test f7prefix][a red]$m[a][a+ yellow bold][dict get $test f7suffix][a]" } if {$hidden_width_m8 == 0} { - set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ green ][dict get $test f8prefix][a red]$m8[a][a+ green][dict get $test f8suffix][a]" } else { - set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ yellow bold][dict get $test f8prefix][a red]$m8[a][a+ yellow bold][dict get $test f8suffix][a]" } append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" } @@ -7923,7 +8075,7 @@ namespace eval punk { } set widest0 [$t column_datawidth 0] $t configure_column 0 -minwidth [expr {$widest0 + 4}] - append text \n[$t print] + append text \n[$t print] lappend chunks [list stdout $text] } @@ -7933,7 +8085,7 @@ namespace eval punk { proc help {args} { set chunks [help_chunks {*}$args] foreach chunk $chunks { - lassign $chunk chan text + lassign $chunk chan text puts -nonewline $chan $text } } @@ -7963,8 +8115,7 @@ namespace eval punk { interp alias {} know {} punk::know interp alias {} know? {} punk::know? - #interp alias {} arg {} punk::val - interp alias {} val {} punk::val + #interp alias {} val {} punk::val interp alias {} exitcode {} punk::exitcode interp alias {} hide {} punkapp::hide_console ;#will only work if controllable toplevels exist @@ -7979,7 +8130,7 @@ namespace eval punk { - + #friendly sh aliases (which user may wish to disable e.g if conflicts) interp alias {} test {} punk::sh_test ;#not much reason to run 'test' directly in punk shell (or tclsh shell) as returncode not obvious anyway due to use of exec interp alias {} TEST {} punk::sh_TEST; #double-evaluation to return tcl true/false from exitcode @@ -8016,7 +8167,7 @@ namespace eval punk { #---------------------------------------------- interp alias {} linelistraw {} punk::linelistraw - + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? interp alias {} PATH {} punk::path @@ -8066,13 +8217,13 @@ namespace eval punk { # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion interp alias {} l {} sh_runout -n ls -A ;#plain text listing - #interp alias {} ls {} sh_runout -n ls -AF --color=always + #interp alias {} ls {} sh_runout -n ls -AF --color=always interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less #note that shell globbing with * won't work on unix systems when using unknown/exec interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? - #interp alias {} lw {} ls -aFv --color=always + #interp alias {} lw {} ls -aFv --color=always interp alias {} dir {} shellrun::runconsole dir @@ -8093,7 +8244,7 @@ namespace eval punk { interp alias {} ./~ {} punk::nav::fs::d/~ interp alias {} d/~ {} punk::nav::fs::d/~ interp alias "" x/ "" punk::nav::fs::x/ - + if {$::tcl_platform(platform) eq "windows"} { set has_powershell 1 @@ -8101,10 +8252,10 @@ namespace eval punk { interp alias {} dw {} dir /W/D } else { #todo - natsorted equivalent - #interp alias {} dl {} + #interp alias {} dl {} interp alias {} dl {} puts stderr "not implemented" interp alias {} dw {} puts stderr "not implemented" - #todo - powershell detection on other platforms + #todo - powershell detection on other platforms set has_powershell 0 } if {$has_powershell} { @@ -8142,7 +8293,7 @@ namespace eval punk { if {[punk::repl::codethread::is_running]} { puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" set ::repl::done 1 - } + } } start { if {[punk::repl::codethread::is_running]} { @@ -8167,8 +8318,8 @@ punk::mod::cli set_alias app #todo - change to punk::dev package require punk::mix -punk::mix::cli set_alias dev -punk::mix::cli set_alias deck ;#deprecate! +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! #todo - add punk::deck for managing cli modules and commandsets diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm index fd638812..b8fada0b 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -118,6 +118,7 @@ tcl::namespace::eval punk::aliascore { pdict ::punk::lib::pdict\ plist {::punk::lib::pdict -roottype list}\ showlist {::punk::lib::showdict -roottype list}\ + grepstr ::punk::grepstr\ rehash ::punk::rehash\ showdict ::punk::lib::showdict\ ansistrip ::punk::ansi::ansistrip\ @@ -136,6 +137,7 @@ tcl::namespace::eval punk::aliascore { rmcup ::punk::console::disable_alt_screen\ config ::punk::config\ s ::punk::ns::synopsis\ + eg ::punk::ns::eg\ ] #*** !doctools diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm index b8d172da..6b04827d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -611,7 +611,7 @@ tcl::namespace::eval punk::ansi { } ""] proc example {args} { - set argd [punk::args::get_by_id ::punk::ansi::example $args] + set argd [punk::args::parse $args withid ::punk::ansi::example] set colwidth [dict get $argd opts -colwidth] if {[info commands file] eq ""} { error "file command unavailable - punk::ansi::example cannot be shown" @@ -723,7 +723,8 @@ tcl::namespace::eval punk::ansi { } lappend adjusted_row $i } - append result [textblock::join_basic -- {*}$adjusted_row] \n + #append result [textblock::join_basic -- {*}$adjusted_row] \n + append result [textblock::join_basic_raw {*}$adjusted_row] \n incr rowindex } @@ -876,6 +877,7 @@ tcl::namespace::eval punk::ansi { tlc l\ trc k\ blc m\ + brc j\ ltj t\ rtj u\ ttj w\ @@ -985,51 +987,51 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #css 1-2.0 HTML 3.2-4 Basic colours eg web-silver for fg Web-silver for bg # variable WEB_colour_map_basic - tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF - tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 - tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 - tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 - tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 - tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 - tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 - tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 - tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 - tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF - tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 - tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF - tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 - tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF - tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 + tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 + tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 + tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 + tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 + tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF + tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 # -- --- --- #Pink colours variable WEB_colour_map_pink - tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 - tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 - tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 - tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 - tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 - tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB + tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 + tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 + tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 + tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 + tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 + tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB # -- --- --- #Red colours variable WEB_colour_map_red - tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 - tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 - tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C - tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C - tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 - tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 - tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A - tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A + tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 + tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 + tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C + tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C + tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 + tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 + tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A + tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A # -- --- --- #Orange colours variable WEB_colour_map_orange - tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 - tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 - tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 - tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 - tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 + tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 + tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 + tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 + tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 + tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 # -- --- --- #Yellow colours variable WEB_colour_map_yellow @@ -1041,7 +1043,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_yellow palegoldenrod 238-232-170 ;# #EEE8AA tcl::dict::set WEB_colour_map_yellow moccasin 255-228-181 ;# #FFE4B5 tcl::dict::set WEB_colour_map_yellow papayawhip 255-239-213 ;# #FFEFD5 - tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 + tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 tcl::dict::set WEB_colour_map_yellow lemonchiffon 255-250-205 ;# #FFFACD tcl::dict::set WEB_colour_map_yellow lightyellow 255-255-224 ;# #FFFFE0 # -- --- --- @@ -1068,7 +1070,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Purple, violet, and magenta colours variable WEB_colour_map_purple tcl::dict::set WEB_colour_map_purple indigo 75-0-130 ;# #4B0082 - tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 tcl::dict::set WEB_colour_map_purple darkmagenta 139-0-139 ;# #8B008B tcl::dict::set WEB_colour_map_purple darkviolet 148-0-211 ;# #9400D3 tcl::dict::set WEB_colour_map_purple darkslateblue 72-61-139 ;# #9400D3 @@ -1089,10 +1091,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Blue colours variable WEB_colour_map_blue tcl::dict::set WEB_colour_map_blue midnightblue 25-25-112 ;# #191970 - tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 tcl::dict::set WEB_colour_map_blue darkblue 0-0-139 ;# #00008B tcl::dict::set WEB_colour_map_blue mediumblue 0-0-205 ;# #0000CD - tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF tcl::dict::set WEB_colour_map_blue royalblue 65-105-225 ;# #4169E1 tcl::dict::set WEB_colour_map_blue steelblue 70-130-180 ;# #4682B4 tcl::dict::set WEB_colour_map_blue dodgerblue 30-144-255 ;# #1E90FF @@ -1113,7 +1115,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_cyan darkturquoise 0-206-209 ;# #00CED1 tcl::dict::set WEB_colour_map_cyan mediumturquoise 72-209-204 ;# #48D1CC tcl::dict::set WEB_colour_map_cyan turquoise 64-224-208 ;# #40E0D0 - tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF tcl::dict::set WEB_colour_map_cyan cyan 0-255-255 ;# #00FFFF - same as aqua tcl::dict::set WEB_colour_map_cyan aquamarine 127-255-212 ;# #7FFFD4 tcl::dict::set WEB_colour_map_cyan paleturquoise 175-238-238 ;# #AFEEEE @@ -1126,11 +1128,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_green darkolivegreen 85-107-47 ;# #55682F tcl::dict::set WEB_colour_map_green forestgreen 34-139-34 ;# #228B22 tcl::dict::set WEB_colour_map_green seagreen 46-139-87 ;# #2E8B57 - tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 tcl::dict::set WEB_colour_map_green olivedrab 107-142-35 ;# #6B8E23 tcl::dict::set WEB_colour_map_green mediumseagreen 60-179-113 ;# #3CB371 tcl::dict::set WEB_colour_map_green limegreen 50-205-50 ;# #32CD32 - tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 tcl::dict::set WEB_colour_map_green springgreen 0-255-127 ;# #00FF7F tcl::dict::set WEB_colour_map_green mediumspringgreen 0-250-154 ;# #00FA9A tcl::dict::set WEB_colour_map_green darkseagreen 143-188-143 ;# #8FBC8F @@ -1160,15 +1162,15 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_white mintcream 245-255-250 ;# #F5FFFA tcl::dict::set WEB_colour_map_white snow 255-250-250 ;# #FFFAFA tcl::dict::set WEB_colour_map_white ivory 255-255-240 ;# #FFFFF0 - tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF # -- --- --- #Gray and black colours variable WEB_colour_map_gray - tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 tcl::dict::set WEB_colour_map_gray darkslategray 47-79-79 ;# #2F4F4F tcl::dict::set WEB_colour_map_gray dimgray 105-105-105 ;# #696969 tcl::dict::set WEB_colour_map_gray slategray 112-128-144 ;# #708090 - tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 tcl::dict::set WEB_colour_map_gray lightslategray 119-136-153 ;# #778899 tcl::dict::set WEB_colour_map_gray darkgray 169-169-169 ;# #A9A9A9 tcl::dict::set WEB_colour_map_gray silver 192-192-192 ;# #C0C0C0 @@ -1201,6 +1203,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set X11_colour_map [tcl::dict::merge $WEB_colour_map $X11_colour_map_diff] + + + + #Xterm colour names (256 colours) #lists on web have duplicate names #these have been renamed here in a systematic way: @@ -1217,6 +1223,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #The xterm names are boringly unimaginative - and also have some oddities such as: # DarkSlateGray1 which looks much more like cyan.. # The greyxx names are spelt with an e - but the darkslategrayX variants use an a. Perhaps that's because they are more cyan than grey and the a is a hint? + #(more likely just a mix of UK vs US spelling) # there is no gold or gold2 - but there is gold1 and gold3 #but in general the names bear some resemblance to the colours and are at least somewhat intuitive. @@ -1612,7 +1619,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set fg "black" } } - lappend clist "[a+ {*}$fc {*}$fg Term$i][format %3s $i]" + lappend clist "[a+ {*}$fc {*}$fg Term-$i][format %3s $i]" } set t [textblock::list_as_table -columns 36 -return tableobject $clist] @@ -1636,7 +1643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 8} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] } @@ -1698,7 +1705,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { set br "" } - append out "$br[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "$br[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } append out [a] return [tcl::string::trimleft $out \n] @@ -1723,7 +1730,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_seps 0 -show_edge 0 for {set i 16} {$i <=231} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== $cols} { lappend rows $row set row [list] @@ -1792,7 +1799,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu foreach cnum $pastel8 { append p8 "[a+ {*}$fc $fg Term-$cnum][format %3s $cnum] " } - append p8 [a]\n + #append p8 [a]\n + #append out \n $p8 + + append p8 [a] append out \n $p8 return $out @@ -1879,7 +1889,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 243} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] @@ -1899,7 +1909,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_hseps 0 -show_edge 0 for {set i 232} {$i <=255} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== 8} { lappend rows $row set row [list] @@ -1919,6 +1929,169 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return [tcl::string::trimleft $out \n] } + + if {[catch {package require punk::ansi::colourmap} errM]} { + puts stderr "punk::ansi FAILED to load punk::ansi::colourmap\n$errM" + } + if {[info exists ::punk::ansi::colourmap::TK_colour_map]} { + upvar ::punk::ansi::colourmap::TK_colour_map TK_colour_map + upvar ::punk::ansi::colourmap::TK_colour_map_lookup TK_colour_map_lookup + } else { + puts stderr "Failed to find TK_colour_map - punk::ansi::colourmap package not loaded?" + variable TK_colour_map {} + variable TK_colour_map_lookup {} + } + + #order-preserving + proc lunique {list} { + set new {} + foreach item $list { + if {$item ni $new} { + lappend new $item + } + } + return $new + } + + proc colourtable_tk {args} { + set opts {-forcecolour 0 -groups * -merged 0 -globs *} + foreach {k v} $args { + switch -- $k { + -groups - -merged - -forcecolour - -globs { + tcl::dict::set opts $k $v + } + default { + error "colourtable_tk unrecognised option '$k'. Known-options: [tcl::dict::keys $opts]" + } + } + } + set fc "" + if {[tcl::dict::get $opts -forcecolour]} { + set fc "forcecolour" + } + + #not implemented - todo? Tk + set groups [tcl::dict::get $opts -groups] + + set do_merge [tcl::dict::get $opts -merged] + set globs [tcl::dict::get $opts -globs] + + + + set blocklist [list] ;#vertical blocks consisting of blockrows + set blockrow [list] + set height 50 ;#number of lines (excluding header) vertically in a blockrow + set columns 5 ;#number of columns in a blockrow + variable TK_colour_map ;#use the version without lowercased additions - this gives the display names with casing as shown in Tk colour man page. + if {!$do_merge} { + set map $TK_colour_map + if {$globs eq "*"} { + set keys [dict keys $TK_colour_map] + } else { + set keys [list] + set mapkeys [dict keys $TK_colour_map] + foreach g $globs { + #lappend keys {*}[dict keys $map $g] + #need case insensitive globs for convenience. + lappend keys {*}[lsearch -all -glob -inline -nocase $mapkeys $g] + } + set keys [lunique $keys] + } + } else { + #todo - make glob fully search when do_merge + #needs to get keys from all names - but then map to keys that have dependent names + upvar ::punk::ansi::colourmap::TK_colour_map_merge map + upvar ::punk::ansi::colourmap::TK_colour_map_reverse reversemap + if {$globs eq "*"} { + set keys [dict keys $map] + } else { + set keys [list] + set allkeys [dict keys $TK_colour_map] + + foreach g $globs { + set matchedkeys [lsearch -all -glob -inline -nocase $allkeys $g] + foreach m $matchedkeys { + if {![dict exists $map $m]} { + #not a parent in a merge + set rgb [dict get $TK_colour_map $m] + set names [dict get $reversemap $rgb] + #first name is the one that is in the merge map + lappend keys [lindex $names 0] + } else { + lappend keys $m + } + } + } + set keys [lunique $keys] + } + } + set overheight 0 + + + set t "" + set start 0 + set colidx -1 + set i -1 + foreach cname $keys { + incr i + set data [dict get $map $cname] + if {$overheight || $i % $height == 0} { + set overheight 0 + incr colidx + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + $t destroy + if {$colidx % $columns == 0} { + lappend blocklist $blockrow + set blockrow [list] + } + } + set start $i + set t [textblock::class::table new] + $t configure -show_edge 0 -show_seps 0 -show_header 1 -minwidth 42 + } + if {!$do_merge} { + set cdec $data + $t add_row [list $cname " [colour_dec2hex $cdec] " $cdec] + } else { + set cdec [dict get $data colour] + set othernames [dict get $data names] + set ndisplay [join [list $cname {*}$othernames] \n] + $t add_row [list $ndisplay " [colour_dec2hex $cdec] " $cdec] + set overheight 0 + foreach n $othernames { + incr i + if {$i % $height == 0} { + set overheight 1 + } + } + } + set fg "rgb-$cdec-contrasting" + $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] + } + + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + lappend blocklist $blockrow + $t destroy + } + + set result "" + foreach blockrow $blocklist { + append result [textblock::join -- {*}$blockrow] \n + } + + return $result + } + #set WEB_colour_map [tcl::dict::merge\ # $WEB_colour_map_basic\ # $WEB_colour_map_pink\ @@ -1970,17 +2143,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } set grouptables [list] - set white_fg_list [list\ - mediumvioletred deeppink\ - darkred red firebrick crimson indianred\ - orangered\ - maroon brown saddlebrown sienna\ - indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ - midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ - teal darkcyan\ - darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ - black darkslategray dimgray slategray\ - ] + #set white_fg_list [list\ + # mediumvioletred deeppink\ + # darkred red firebrick crimson indianred\ + # orangered\ + # maroon brown saddlebrown sienna\ + # indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ + # midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ + # teal darkcyan\ + # darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ + # black darkslategray dimgray slategray\ + # ] foreach g $show_groups { #upvar WEB_colour_map_$g map_$g variable WEB_colour_map_$g @@ -1988,11 +2161,12 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t configure -show_edge 0 -show_seps 0 -show_header 1 tcl::dict::for {cname cdec} [set WEB_colour_map_$g] { $t add_row [list "$cname " "[colour_dec2hex $cdec] " $cdec] - if {$cname in $white_fg_list} { - set fg "web-white" - } else { - set fg "web-black" - } + set fg "rgb-$cdec-contrasting" + #if {$cname in $white_fg_list} { + # set fg "web-white" + #} else { + # set fg "web-black" + #} #$t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Web-$cname] } @@ -2083,12 +2257,66 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return $displaytable } + lappend PUNKARGS [list { + @id -id ::punk::ansi::a? + @cmd -name "punk::ansi::a?"\ + -summary\ + "ANSI colour information"\ + -help\ + "" + @form -form "sgr_overview" + @values -form "sgr_overview" -min 0 -max 0 + + + @form -form "term" + @leaders -form "term" -min 1 -max 1 + term -type literal(term) -help\ + "256 term colours" + @opts -min 0 -max 0 + @values -form "term" -min 0 -max -1 + panel -type string -optional 1 -multiple 1 -default {16 main greyscale note}\ + -choices {16 main greyscale pastel rainbow note} + + @form -form "tk" + @leaders -form "tk" -min 1 -max 1 + tk -type literal(tk)|literal(TK) -help\ + "Tk colours" + @opts -form "tk" + -merged -type none -help\ + "If this flag is supplied - show colour names with the same RGB + values together." + @values -form "tk" -min 0 -max -1 + glob -type string -optional 1 -multiple 1 -help\ + "A glob string such as *green*. + Multiple glob entries can be provided. The search is case insensitive" + + + @form -form "web" + @values -form "web" -min 1 -max -1 + web -type literal(web) -help\ + "Web colours" + panel -type string -optional 1 -multiple 1 -choices {basic pink red orange yellow brown purple blue cyan green white gray} + + @form -form "x11" + @values -form "x11" -min 1 -max 1 + x11 -type literal(x11) -help\ + "x11 colours" + + + @form -form "sample" + @values -form "sample" -min 1 -max -1 + colourcode -type sgr|Sgr|literalprefix(term-)|literalprefix(Term-)|literalprefix(web-)|literalprefix(Web-)|literalprefix(rgb)|literalprefix(Rgb)\ + -optional 0\ + -multiple 1 + + }] proc a? {args} { #*** !doctools #[call [fun a?] [opt {ansicode...}]] #[para]Return an ansi string representing a table of codes and a panel showing the colours variable SGR_setting_map variable SGR_colour_map + variable TK_colour_map_lookup set fcposn [lsearch $args "force*"] set fc "" set opt_forcecolour 0 @@ -2172,6 +2400,20 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu append out \n append out "[a+ {*}$fc web-white]X11 colours[a] - mostly match Web colours" \n append out [textblock::join -- $indent "To see differences: a? x11"] \n + append out \n + append out "[a+ {*}$fc web-white]Tk colours[a]" \n + append out [textblock::join -- $indent "To see all 750+ names use: a? tk"] \n + append out [textblock::join -- $indent "Restrict the results using globs e.g a? tk *green* *yellow*"] \n + append out [textblock::join -- $indent "The foreground colour in this table is generated using the contrasting suffix"] \n + append out [textblock::join -- $indent "Example: \[a+ tk-tan-contrasting Tk-tan\]text\[a] -> [a+ {*}$fc tk-tan-contrasting Tk-tan]text[a]"] \n + append out \n + append out "[a+ {*}$fc web-white]Combination testing[a]" \n + append out [textblock::join -- $indent "Example: a? red brightgreen underline Tk-slategrey italic"] \n + append out [textblock::join -- $indent "This will show a small table of each applied code and a RESULT row. The 'red' in this case is redundant,"] \n + append out [textblock::join -- $indent "so a final MERGED row displays with an alert 'REDUNDANCIES FOUND'."] \n + append out [textblock::join -- $indent "The final columns of RESULT and MERGED (showing raw ANSI sequence) will differ if the arguments aren't in canonical order."] \n + append out [textblock::join -- $indent "The MERGED line will only display if there are redundancies or different ordering."] \n + if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { append out \n @@ -2191,40 +2433,74 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { switch -- [lindex $args 0] { term { - set termargs [lrange $args 1 end] - foreach ta $termargs { - switch -- $ta { - pastel - rainbow {} - default {error "unrecognised term option '$ta'. Known values: pastel rainbow"} - } - } - set out "16 basic colours\n" - append out [colourtable_16_names -forcecolour $opt_forcecolour] \n - append out "216 colours\n" - append out [colourtable_216_names -forcecolour $opt_forcecolour] \n - append out "24 greyscale colours\n" - append out [colourtable_24_names -forcecolour $opt_forcecolour] - foreach ta $termargs { - switch -- $ta { + set argd [punk::args::parse $args -form "term" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + set panels [dict get $values panel] + + set out "" + foreach panel $panels { + #punk::args has already resolved prefixes to full panel names + switch -- $panel { + 16 { + append out "16 basic colours\n" + append out [colourtable_16_names -forcecolour $opt_forcecolour] \n + } + main { + append out "216 colours\n" + append out [colourtable_216_names -forcecolour $opt_forcecolour] \n + } + note { + append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" \n + append out " grey vs gray (UK/US spelling) - these are inconsistent for historical reasons. e.g grey0,lightslategrey,darkslategray1" \n + } + greyscale { + append out "24 greyscale colours\n" + append out [colourtable_24_names -forcecolour $opt_forcecolour] \n + } pastel { - append out \n append out "Pastel Colour Space (punk::ansi::colourtable_term_pastel)\n" - append out [colourtable_term_pastel -forcecolour $opt_forcecolour] + append out [colourtable_term_pastel -forcecolour $opt_forcecolour] \n } rainbow { - append out \n append out "Rainbow Colours (punk::ansi::colourtable_term_rainbow)\n" - append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] + append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] \n + } + default { + #only reachable if punk::args definition is out of sync + set panelnames {16 main greyscale pastel rainbow note} + append out "(ERROR: unrecognised panel '$ta' for 'a? term'. Known values $panelnames)" } } } - append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" return $out } web { - return [colourtable_web -forcecolour $opt_forcecolour -groups [lrange $args 1 end]] + set argd [punk::args::parse $args -form "web" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received panel]} { + set panels [dict get $values panel] + } else { + set panels {*} + } + return [colourtable_web -forcecolour $opt_forcecolour -groups $panels] + } + tk - TK { + set argd [punk::args::parse $args -form "tk" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received glob]} { + set globs [dict get $values glob] + } else { + set globs {*} + } + if {[dict exists $received -merged]} { + set ismerged 1 + } else { + set ismerged 0 + } + return [colourtable_tk -merged $ismerged -forcecolour $opt_forcecolour -globs $globs] } x11 { + set argd [punk::args::parse $args -form "x11" -errorstyle standard withid ::punk::ansi::a?] set out "" append out " Mostly same as web - known differences displayed" \n append out [colourtable_x11diff -forcecolour $opt_forcecolour] @@ -2243,10 +2519,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set resultlist [list] foreach i $args { - set f4 [tcl::string::range $i 0 3] + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [::split $i "-# "] 0] set s [a+ {*}$fc $i]sample - switch -- $f4 { - web- - Web- - WEB- { + switch -- $pfx { + web - Web - WEB { set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] set cont [string range $tail end-11 end] switch -- $cont { @@ -2275,7 +2552,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i $descr $s [ansistring VIEW $s]] } term - Term - undt { - set tail [tcl::string::trim [tcl::string::range $i 4 end] -] + set tail [tcl::string::range $i 5 end] if {[tcl::string::is integer -strict $tail]} { if {$tail < 256} { set descr "[tcl::dict::get $TERM_colour_map_reverse $tail]" @@ -2292,10 +2569,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - x11- - X11- { - set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] - if {[tcl::dict::exists $X11_colour_map $tail]} { - set dec [tcl::dict::get $X11_colour_map $tail] + x11 - X11 { + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] + set cont [string range $cname end-11 end] + switch -exact -- $cont {-contrasting - -contrastive {set cname [string range $tail end-12]}} + + if {[tcl::dict::exists $X11_colour_map $cname]} { + set dec [tcl::dict::get $X11_colour_map $cname] set hex [colour_dec2hex $dec] set descr "$hex $dec" } else { @@ -2303,12 +2583,27 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - rgb- - Rgb- - RGB- - - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 - - rgb# - Rgb# - RGB# - - und# - und- { + tk - Tk { + set tail [tcl::string::tolower [tcl::string::range $i 3 end]] + set cont [string range $tail end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set cname [string range $tail 0 end-12] + } + default { + set cname $tail + } + } + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set dec [tcl::dict::get $TK_colour_map_lookup $cname] + set hex [colour_dec2hex $dec] + set descr "$hex $dec" + } else { + set descr "UNKNOWN colour for tk" + } + $t add_row [list $i $descr $s [ansistring VIEW $s]] + } + rgb - Rgb - RGB - und { set cont [string range $i end-11 end] switch -- $cont { -contrasting - -contrastive { @@ -2339,7 +2634,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } set info "$hexfinal $decfinal" ;#show opposite type as first line of info col } else { - set tail [tcl::string::trim [tcl::string::range $iplain 3 end] -] + set tail [tcl::string::range $iplain 4 end] set dec $tail switch -- $cont { -contrasting { @@ -2369,15 +2664,23 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend x11colours $c } } + if {[dict exists $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal]} { + set tkcolours [dict get $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal] + } else { + set tkcolours [list] + } foreach c $webcolours { append info \n web-$c } foreach c $x11colours { append info \n x11-$c } + foreach c $tkcolours { + append info \n tk-$c + } $t add_row [list $i "$info" $s [ansistring VIEW $s]] } - unde { + default { switch -- $i { undercurly - undercurl - underdotted - underdot - underdashed - underdash - undersingle - underdouble { $t add_row [list $i extended $s [ansistring VIEW $s]] @@ -2389,19 +2692,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i "SGR 59" $s [ansistring VIEW $s]] } default { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] - } - } - } - default { - if {[tcl::string::is integer -strict $i]} { - set rmap [lreverse $SGR_map] - $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] - } else { - if {[tcl::dict::exists $SGR_map $i]} { - $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] - } else { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + #$t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + if {[tcl::string::is integer -strict $i]} { + set rmap [lreverse $SGR_map] + $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] + } else { + if {[tcl::dict::exists $SGR_map $i]} { + $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] + } else { + $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + } + } } } } @@ -2541,24 +2842,19 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes needing to go in own escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + set pfx [lindex [::split $i "-# "] 0] + #set f4 [tcl::string::range $i 0 3] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour - set tail [tcl::string::tolower [tcl::string::range $i 4 end]] + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] #-contrasting #-contrastive - set cont [string range $tail end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set cname [string range $tail 0 end-12] - } - default { - set cname $tail - } - } + set cont [string range $cname end-11 end] + switch -- $cont { -contrasting - -contrastive {set cname [string range $cname 0 end-12]} } + if {[tcl::dict::exists $WEB_colour_map $cname]} { set rgbdash [tcl::dict::get $WEB_colour_map $cname] switch -- $cont { @@ -2577,7 +2873,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a+ $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -2609,140 +2905,94 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a+ $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6 } + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - #TODO - fix - # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. - # need to emit in - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #NOTE struct::set result order can differ depending on whether tcl/critcl imp used - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline {lappend t 4} + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #NOTE struct::set result order can differ depending on whether tcl/critcl imp used + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + #TODO - fix + # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. + # need to emit in + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund {lappend t 59} + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer @@ -2772,105 +3022,112 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb foreground/background - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set iplain [string range $i 0 end-12] + rgb - Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground/background + #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set iplain [string range $i 0 end-12] + } + default { + set iplain $i + } } - default { - set iplain $i + set rgbspec [tcl::string::range $iplain 4 end] + set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - } - set rgbspec [tcl::string::trim [tcl::string::range $iplain 3 end] -] - set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + if {[tcl::string::index $i 0] eq "r"} { + #fg + lappend t "38;2;$rgbfinal" + } else { + #bg + lappend t "48;2;$rgbfinal" } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + + } elseif {$utype eq "#"} { + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - default { - set rgbfinal [join $RGB {;}] + if {[tcl::string::index $i 0] eq "r"} { + #hex rgb foreground + lappend t "38;2;$rgbfinal" + } else { + #hex rgb background + lappend t "48;2;$rgbfinal" } - } - if {[tcl::string::index $i 0] eq "r"} { - #fg - lappend t "38;2;$rgbfinal" } else { - #bg - lappend t "48;2;$rgbfinal" + puts stderr "punk::ansi::a+ ansi term rgb colour unmatched: '$i' in call 'a+ $args'" } } - "rgb#" - "Rgb#" - "RGB#" { - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx + #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb + set rgbspec [tcl::string::range $i 4 end] + set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] + #puts "---->'$RGB'<----" + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - default { - set rgbfinal [join $RGB {;}] + #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? + lappend e "58:2::$rgbfinal" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - } - if {[tcl::string::index $i 0] eq "r"} { - #hex rgb foreground - lappend t "38;2;$rgbfinal" + lappend e "58:2::$rgbfinal" } else { - #hex rgb background - lappend t "48;2;$rgbfinal" - } - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] - #puts "---->'$RGB'<----" - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } - } - #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? - lappend e "58:2::$rgbfinal" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } + puts stderr "punk::ansi::a+ ansi term underline colour unmatched: '$i' in call 'a+ $args'" } - lappend e "58:2::$rgbfinal" } undt { #CSI 58:5 UNDERLINE COLOR PALETTE INDEX @@ -2878,7 +3135,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -2889,7 +3146,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2898,10 +3155,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i' in call 'a+ $args'" + puts stderr "ansi x11 foreground colour unmatched: '$i' in call 'a+ $args'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2910,7 +3167,59 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + #foreground tk names + variable TK_colour_map_lookup ;#use the dict with added lowercase versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -exact -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i' in call 'a+ $args'" + } + } + Tk - TK { + #background X11 names + variable TK_colour_map_lookup ;#with lc versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -2919,7 +3228,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a+ ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- tk- term- rgb# rgb-" } } } @@ -2974,6 +3283,32 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #indent of 1 space is important for clarity in i -return string a+ output dict set SGR_samples $k " [punk::ansi::a+ $k]sample\x1b\[m" } + set SGR_help\ + {SGR code from the list below, or an integer corresponding to the code e.g 31 = red. + A leading capital letter indicates a codename applies to the background colour. + Other accepted codes are: + term- Term- foreground/background where int is 0-255 terminal color + term- Term- foreground/background + + web- Web- + + x11- X11- + + tk- Tk- + + rgb--- Rgb--- foreground/background where are the + 0-255 int values for red, green and blue. + rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 + + The acceptable values for colours can be queried using + punk::ansi::a? term + punk::ansi::a? web + punk::ansi::a? x11 + punk::ansi::a? tk + + Example to set foreground red and background cyan followed by a reset: + set str \"[a+ red Cyan]sample text[a]\" + } lappend PUNKARGS [list { @id -id ::punk::ansi::a+ @cmd -name "punk::ansi::a+" -help\ @@ -2981,28 +3316,25 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Unlike punk::ansi::a - it is not prefixed with an ANSI reset. " @values -min 0 -max -1 - } [string map [list [dict keys $SGR_map] $SGR_samples] { - code -type string -optional 1 -multiple 1 -choices {}\ - -choicelabels {}\ + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ - "SGR code from the list below, or an integer corresponding to the code e.g 31 = red. - A leading capital letter indicates a codename applies to the background colour. - Other accepted codes are: - term- Term- foreground/background where int is 0-255 terminal color - term- Term- foreground/background - rgb--- Rgb--- foreground/background where are the - 0-255 int values for red, green and blue. - rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 - web- Web- - - The acceptable values for and can be queried using - punk::ansi::a? term - and - punk::ansi::a? web - - Example to set foreground red and background cyan followed by a reset: - set str \"[a+ red Cyan]sample text[a]\" + "%SGR_help%" + }]] + + lappend PUNKARGS [list { + @id -id ::punk::ansi::a + @cmd -name "punk::ansi::a" -help\ + "Returns an ANSI sgr escape sequence based on the list of supplied codes. + Unlike punk::ansi::a+ - it is prefixed with an ANSI reset. " + @values -min 0 -max -1 + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ + -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ + "%SGR_help%" }]] proc a {args} { @@ -3027,6 +3359,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #we want this to be available to call even if ansi is off variable WEB_colour_map variable TERM_colour_map + variable TK_colour_map_lookup ;#Tk accepts lowercase versions of colours even though some colours are documented with casing set colour_disabled 0 #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache -action clear @@ -3044,9 +3377,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes will suppress standard SGR colours and attributes if merged in same escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [split $i "-# "] 0] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour @@ -3059,7 +3393,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -3070,142 +3404,100 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6} + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline { + lappend t 4 ;#underline + } + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund { + lappend t 59 + } + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend t "38;5;$cc" } else { @@ -3219,7 +3511,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Term - TERM { #variable TERM_colour_map #256 colour background by Xterm name or by integer - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] && $cc < 256} { lappend t "48;5;$cc" } else { @@ -3230,49 +3522,63 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 { - #decimal rgb foreground - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "38;2;$rgb" - } - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb background - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "48;2;$rgb" - } - "rgb#" { - #hex rgb foreground - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "38;2;$rgb" - } - "Rgb#" - "RGB#" { - #hex rgb background - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "48;2;$rgb" - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] - lappend e "58:2::$rgb" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - lappend e "58:2::$rgb" + rgb { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground + #form: rgb-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "38;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb foreground + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi rgb foreground colour unmatched: '$i' in call 'a $args'" + } + } + Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb background + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "48;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb background + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Rgb background colour unmatched: '$i' in call 'a $args'" + } + } + und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 {} + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #form: und-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] + lappend e "58:2::$rgb" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + lappend e "58:2::$rgb" + } else { + puts stderr "ansi underline colour unmatched: '$i' in call 'a $args'" + } } undt { #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + #undt-<0-255> or undt- + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -3283,7 +3589,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3292,10 +3598,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i'" + puts stderr "ansi x11 foreground colour unmatched: '$i'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3304,7 +3610,31 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + variable TK_colour_map_lookup + #foreground tk names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i'" + } + } + Tk - TK { + variable TK_colour_map_lookup + #background X11 names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -3313,7 +3643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" } } } @@ -3356,7 +3686,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend PUNKARGS [list { @id -id ::punk::ansi::ansiwrap - @cmd -name punk::ansi::ansiwrap -help\ + @cmd -name punk::ansi::ansiwrap\ + -summary\ + "Wrap a string with ANSI codes applied when not overridden by ANSI in the source string."\ + -help\ {Wrap a string with ANSI codes from supplied codelist(s) followed by trailing ANSI reset. The wrapping is done such that @@ -3395,12 +3728,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu -rawansi -type ansi -default "" -resetcodes -type list -default {reset} -rawresets -type ansi -default "" - -fullcodemerge -type boolean -default 0 -help\ - "experimental" -overridecodes -type list -default {} -rawoverrides -type ansi -default "" @values -min 1 -max 1 - text -type string -help\ + text -type any -help\ "String to wrap with ANSI (SGR)" }] proc ansiwrap {args} { @@ -3411,13 +3742,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } #we know there are no valid codes that start with - if {[lsearch [lrange $args 0 end-1] -*] == -1} { - #no opts - set text [lindex $args end] - set codelists [lrange $args 0 end-1] - set R [a] ;#plain ansi reset + #no opts - skip args parser + #maint: keep defaults in sync with definition above + set codelists $args + set text [lpop codelists] + set R [a] ;#plain ansi reset (equiv of default "reset") set rawansi "" set rawresets "" - set fullmerge 0 set overrides "" set rawoverrides "" } else { @@ -3428,7 +3759,6 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rawansi [dict get $opts -rawansi] set R [a+ {*}[dict get $opts -resetcodes]] set rawresets [dict get $opts -rawresets] - set fullmerge [dict get $opts -fullcodemerge] set overrides [punk::ansi::ta::get_codes_single [a+ {*}[dict get $opts -overridecodes]]] set rawoverrides [punk::ansi::ta::get_codes_single [dict get $opts -rawoverrides]] } @@ -3437,22 +3767,18 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes set codes [concat {*}$codelists] ;#flatten set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] if {$rawansi ne ""} { set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] - if {$fullmerge} { - set base [punk::ansi::codetype::sgr_merge [list $base {*}$rawcodes]] - } else { - set base [punk::ansi::codetype::sgr_merge_singles [list $base {*}$rawcodes]] - } + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] } if {$rawresets ne ""} { set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] - if {$fullmerge} { - set R [punk::ansi::codetype::sgr_merge [list $R {*}$rawresetcodes]] - } else { - set R [punk::ansi::codetype::sgr_merge_singles [list $R {*}$rawresetcodes]] - } + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] } + if {$rawoverrides ne ""} { set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] set overrides [list {*}$overrides {*}$rawoverridecodes] @@ -3474,20 +3800,105 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set codestack [list] } else { #append emit [lindex $o_codestack 0]$pt - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + #parts ends on a pt - last code always empty string + if {$code ne ""} { + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $codestack $code] + set codestack [lremove $codestack {*}$dup_posns] + lappend codestack $code } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } } } + default { + #other ansi codes + } } - default { - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit $code + } + } + return [append emit $R] + } else { + return $base$text$R + } + } + proc ansiwrap_raw {rawansi rawresets rawoverrides text} { + set codelists "" + set R "" + set overrides "" + #note that the result of any sgr_merge or sgr_merge_singles is not necessarily a single Ansi escape sequence. + #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes + set codes [concat {*}$codelists] ;#flatten + set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] + if {$rawansi ne ""} { + set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] + } + if {$rawresets ne ""} { + set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] + } + + if {$rawoverrides ne ""} { + set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] + set overrides [list {*}$overrides {*}$rawoverridecodes] + } + + set codestack [list] + if {[punk::ansi::ta::detect $text]} { + set emit "" + #set parts [punk::ansi::ta::split_codes $text] + set parts [punk::ansi::ta::split_codes_single $text] + foreach {pt code} $parts { + switch -- [llength $codestack] { + 0 { + append emit $base $pt $R + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $codestack 0]]} { + append emit $base $pt $R + set codestack [list] } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R } } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } } #parts ends on a pt - last code always empty string if {$code ne ""} { @@ -3533,6 +3944,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { return $base$text$R } + } proc ansiwrap_naive {codes text} { return [a_ {*}$codes]$text[a] @@ -4481,6 +4893,20 @@ to 223 (=255 - 32) } #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks + lappend PUNKARGS [list { + @id -id ::punk::ansi::ansistrip + @cmd -name punk::ansi::ansistrip\ + -summary\ + "Strip ANSI codes and convert VT100 graphics to unicode equivalents."\ + -help\ + "Returns a string with ANSI codes such as SGR, movements etc stripped out. + Alternate graphics chars (VT100 graphics) are replaced with modern unicode + equivalents (e.g boxdrawing glyphs). + PM, APC, SOS contents are stripped - whether or not such wrapped strings + are displayed on various terminals." + @values -min 1 -max 1 + text -type string + }] proc ansistrip {text} { #*** !doctools #[call [fun ansistrip] [arg text] ] @@ -7586,7 +8012,7 @@ tcl::namespace::eval punk::ansi::ansistring { #return pair of column extents occupied by the character index supplied. #single-width grapheme will return pair of integers of equal value - #doulbe-width grapheme will return a pair of consecutive indices + #double-width grapheme will return a pair of consecutive indices proc INDEXCOLUMNS {string idx} { #There is an index per grapheme - whether it is 1 or 2 columns wide set index [lindex [INDEXABSOLUTE $string $idx] 0] @@ -7755,6 +8181,31 @@ namespace eval punk::ansi::colour { } punk::assertion::active on + + #see also the tk function + #winfo rgb . |#XXXXXX|#XXX + #(example in punk::ansi::colourmap::get_rgb_using_tk) + + #proc percent2rgb {n} { + # # map 0..100 to a red-yellow-green sequence + # set n [expr {$n < 0? 0: $n > 100? 100: $n}] + # set red [expr {$n > 75? 60 - ($n * 15 / 25) : 15}] + # set green [expr {$n < 50? $n * 15 / 50 : 15}] + # format "#%01x%01x0" $red $green + #} ;#courtesy of RS (from tcl wiki) + proc percent2#rgb {n} { + # map 0..100 to a red-yellow-green sequence + set n [expr {$n < 0? 0: $n > 100? 100: $n}] + set red [expr {$n > 75? 1020 - ($n * 255 / 25) : 255}] + set green [expr {$n < 50? $n * 255 / 50 : 255}] + format "#%02x%02x00" $red $green + } + + proc random#rgb {} { + format #%06x [expr {int(rand() * 0xFFFFFF)}] + } + + #see also colors package #https://sourceforge.net/p/irrational-numbers/code/HEAD/tree/pkgs/Colors/trunk/colors.tcl#l159 diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm new file mode 100644 index 00000000..6e8e28e4 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm @@ -0,0 +1,966 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2025 +# +# @@ Meta Begin +# Application ::punk::ansi::colourmap 0.1.0 +# Meta platform tcl +# Meta license MIT +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_::punk::ansi::colourmap 0 0.1.0] +#[copyright "2025"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require ::punk::ansi::colourmap] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of ::punk::ansi::colourmap +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by ::punk::ansi::colourmap +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + + +tcl::namespace::eval ::punk::ansi::colourmap { + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + # Base namespace + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap}] + #[para] Core API functions for ::punk::ansi::colourmap + #[list_begin definitions] + + variable PUNKARGS + + #---------------------------------------------- + #todo - document vars as part of package API + #- or provide a function to return varnames? + #- or wrap each in a function and see if any performance/memory impact? (readonly - so should just be a reference without any copying?) + #TK_colour_map + #TK_colour_map_lookup + #TK_colour_map_merge + #TK_colour_map_reverse + #---------------------------------------------- + + + + #significantly slower than tables - but here as a check/test + lappend PUNKARGS [list { + @id -id ::punk::ansi::colourmap::get_rgb_using_tk + @cmd -name punk::ansi::colourmap::get_rgb_using_tk -help\ + "This function requires Tk to function, and will call + 'package require tk' to load it. + The name argument accepts Tk colour names or hex values + in either #XXX or #XXXXXX format. + Tk colour names can be displayed using the command: + punk::ansi::a? tk ?glob..? + + get_rgb_using_tk returns a decimal rgb string delimited with dashes. + e.g + get_rgb_using_tk #FFF + 255-255-255 + get_rgb_using_tk SlateBlue + 106-90-205" + @leaders + name -type string|stringstartswith(#) + }] + proc get_rgb_using_tk {name} { + package require tk + #assuming 'winfo depth .' is always 32 ? + set RGB [winfo rgb . $name] + set rgb [lmap n $RGB {expr {$n / 256}}] + return [join $rgb -] + } + + variable TK_colour_map + tcl::dict::set TK_colour_map "alice blue" 240-248-255 + tcl::dict::set TK_colour_map AliceBlue 240-248-255 + tcl::dict::set TK_colour_map "antique white" 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite1 255-239-219 + tcl::dict::set TK_colour_map AntiqueWhite2 238-223-204 + tcl::dict::set TK_colour_map AntiqueWhite3 205-192-176 + tcl::dict::set TK_colour_map AntiqueWhite4 139-131-120 + tcl::dict::set TK_colour_map aqua 0-255-255 + tcl::dict::set TK_colour_map aquamarine 127-255-212 + tcl::dict::set TK_colour_map aquamarine1 127-255-212 + tcl::dict::set TK_colour_map aquamarine2 118-238-198 + tcl::dict::set TK_colour_map aquamarine3 102-205-170 + tcl::dict::set TK_colour_map aquamarine4 69-139-16 + tcl::dict::set TK_colour_map azure 240-255-255 + tcl::dict::set TK_colour_map azure1 240-255-255 + tcl::dict::set TK_colour_map azure2 224-238-238 + tcl::dict::set TK_colour_map azure3 193-205-205 + tcl::dict::set TK_colour_map azure4 131-139-139 + tcl::dict::set TK_colour_map beige 245-245-220 + tcl::dict::set TK_colour_map bisque 255-228-196 + tcl::dict::set TK_colour_map bisque1 255-228-196 + tcl::dict::set TK_colour_map bisque2 238-213-183 + tcl::dict::set TK_colour_map bisque3 205-183-158 + tcl::dict::set TK_colour_map bisque4 139-125-107 + tcl::dict::set TK_colour_map black 0-0-0 + tcl::dict::set TK_colour_map "blanched almond" 255-235-205 + tcl::dict::set TK_colour_map BlanchedAlmond 255-235-205 + tcl::dict::set TK_colour_map blue 0-0-255 + tcl::dict::set TK_colour_map "blue violet" 138-43-226 + tcl::dict::set TK_colour_map blue1 0-0-255 + tcl::dict::set TK_colour_map blue2 0-0-238 + tcl::dict::set TK_colour_map blue3 0-0-205 + tcl::dict::set TK_colour_map blue4 0-0-139 + tcl::dict::set TK_colour_map BlueViolet 138-43-226 + tcl::dict::set TK_colour_map brown 165-42-42 + tcl::dict::set TK_colour_map brown1 255-64-64 + tcl::dict::set TK_colour_map brown2 238-59-59 + tcl::dict::set TK_colour_map brown3 205-51-51 + tcl::dict::set TK_colour_map brown4 139-35-35 + tcl::dict::set TK_colour_map burlywood 222-184-135 + tcl::dict::set TK_colour_map burlywood1 255-211-155 + tcl::dict::set TK_colour_map burlywood2 238-197-145 + tcl::dict::set TK_colour_map burlywood3 205-170-125 + tcl::dict::set TK_colour_map burlywood4 139-115-85 + tcl::dict::set TK_colour_map "cadet blue" 95-158-160 + tcl::dict::set TK_colour_map CadetBlue 95-158-160 + tcl::dict::set TK_colour_map CadetBlue1 152-245-255 + tcl::dict::set TK_colour_map CadetBlue2 142-229-238 + tcl::dict::set TK_colour_map CadetBlue3 122-197-205 + tcl::dict::set TK_colour_map CadetBlue4 83-134-139 + tcl::dict::set TK_colour_map chartreuse 127-255-0 + tcl::dict::set TK_colour_map chartreuse1 127-255-0 + tcl::dict::set TK_colour_map chartreuse2 118-238-0 + tcl::dict::set TK_colour_map chartreuse3 102-205-0 + tcl::dict::set TK_colour_map chartreuse4 69-139-0 + tcl::dict::set TK_colour_map chocolate 210-105-30 + tcl::dict::set TK_colour_map chocolate1 255-127-36 + tcl::dict::set TK_colour_map chocolate2 238-118-33 + tcl::dict::set TK_colour_map chocolate3 205-102-29 + tcl::dict::set TK_colour_map chocolate4 139-69-19 + tcl::dict::set TK_colour_map coral 255-127-80 + tcl::dict::set TK_colour_map coral1 255-114-86 + tcl::dict::set TK_colour_map coral2 238-106-80 + tcl::dict::set TK_colour_map coral3 205-91-69 + tcl::dict::set TK_colour_map coral4 139-62-47 + tcl::dict::set TK_colour_map "cornflower blue" 100-149-237 + tcl::dict::set TK_colour_map CornflowerBlue 100-149-237 + tcl::dict::set TK_colour_map cornsilk 255-248-220 + tcl::dict::set TK_colour_map cornsilk1 255-248-220 + tcl::dict::set TK_colour_map cornsilk2 238-232-205 + tcl::dict::set TK_colour_map cornsilk3 205-200-177 + tcl::dict::set TK_colour_map cornsilk4 139-136-120 + tcl::dict::set TK_colour_map crimson 220-20-60 + tcl::dict::set TK_colour_map cyan 0-255-255 + tcl::dict::set TK_colour_map cyan1 0-255-255 + tcl::dict::set TK_colour_map cyan2 0-238-238 + tcl::dict::set TK_colour_map cyan3 0-205-205 + tcl::dict::set TK_colour_map cyan4 0-139-139 + tcl::dict::set TK_colour_map "dark blue" 0-0-139 + tcl::dict::set TK_colour_map "dark cyan" 0-139-139 + tcl::dict::set TK_colour_map "dark goldenrod" 184-134-11 + tcl::dict::set TK_colour_map "dark gray" 169-169-169 + tcl::dict::set TK_colour_map "dark green" 0-100-0 + tcl::dict::set TK_colour_map "dark grey" 169-169-169 + tcl::dict::set TK_colour_map "dark khaki" 189-183-107 + tcl::dict::set TK_colour_map "dark magenta" 139-0-139 + tcl::dict::set TK_colour_map "dark olive green" 85-107-47 + tcl::dict::set TK_colour_map "dark orange" 255-140-0 + tcl::dict::set TK_colour_map "dark orchid" 153-50-204 + tcl::dict::set TK_colour_map "dark red" 139-0-0 + tcl::dict::set TK_colour_map "dark salmon" 233-150-122 + tcl::dict::set TK_colour_map "dark sea green" 143-188-143 + tcl::dict::set TK_colour_map "dark slate blue" 72-61-139 + tcl::dict::set TK_colour_map "dark slate gray" 47-79-79 + tcl::dict::set TK_colour_map "dark slate grey" 47-79-79 + tcl::dict::set TK_colour_map "dark turquoise" 0-206-209 + tcl::dict::set TK_colour_map "dark violet" 148-0-211 + tcl::dict::set TK_colour_map DarkBlue 0-0-139 + tcl::dict::set TK_colour_map DarkCyan 0-139-139 + tcl::dict::set TK_colour_map DarkGoldenrod 184-134-11 + tcl::dict::set TK_colour_map DarkGoldenrod1 255-185-15 + tcl::dict::set TK_colour_map DarkGoldenrod2 238-173-14 + tcl::dict::set TK_colour_map DarkGoldenrod3 205-149-12 + tcl::dict::set TK_colour_map DarkGoldenrod4 139-101-8 + tcl::dict::set TK_colour_map DarkGray 169-169-169 + tcl::dict::set TK_colour_map DarkGreen 0-100-0 + tcl::dict::set TK_colour_map DarkGrey 169-169-169 + tcl::dict::set TK_colour_map DarkKhaki 189-183-107 + tcl::dict::set TK_colour_map DarkMagenta 139-0-139 + tcl::dict::set TK_colour_map DarkOliveGreen 85-107-47 + tcl::dict::set TK_colour_map DarkOliveGreen1 202-255-112 + tcl::dict::set TK_colour_map DarkOliveGreen2 188-238-104 + tcl::dict::set TK_colour_map DarkOliveGreen3 162-205-90 + tcl::dict::set TK_colour_map DarkOliveGreen4 110-139-61 + tcl::dict::set TK_colour_map DarkOrange 255-140-0 + tcl::dict::set TK_colour_map DarkOrange1 255-127-0 + tcl::dict::set TK_colour_map DarkOrange2 238-118-0 + tcl::dict::set TK_colour_map DarkOrange3 205-102-0 + tcl::dict::set TK_colour_map DarkOrange4 139-69-0 + tcl::dict::set TK_colour_map DarkOrchid 153-50-204 + tcl::dict::set TK_colour_map DarkOrchid1 191-62-255 + tcl::dict::set TK_colour_map DarkOrchid2 178-58-238 + tcl::dict::set TK_colour_map DarkOrchid3 154-50-205 + tcl::dict::set TK_colour_map DarkOrchid4 104-34-139 + tcl::dict::set TK_colour_map DarkRed 139-0-0 + tcl::dict::set TK_colour_map DarkSalmon 233-150-122 + tcl::dict::set TK_colour_map DarkSeaGreen 43-188-143 + tcl::dict::set TK_colour_map DarkSeaGreen1 193-255-193 + tcl::dict::set TK_colour_map DarkSeaGreen2 180-238-180 + tcl::dict::set TK_colour_map DarkSeaGreen3 155-205-155 + tcl::dict::set TK_colour_map DarkSeaGreen4 105-139-105 + tcl::dict::set TK_colour_map DarkSlateBlue 72-61-139 + tcl::dict::set TK_colour_map DarkSlateGray 47-79-79 + tcl::dict::set TK_colour_map DarkSlateGray1 151-255-255 + tcl::dict::set TK_colour_map DarkSlateGray2 141-238-238 + tcl::dict::set TK_colour_map DarkSlateGray3 121-205-205 + tcl::dict::set TK_colour_map DarkSlateGray4 82-139-139 + tcl::dict::set TK_colour_map DarkSlateGrey 47-79-79 + tcl::dict::set TK_colour_map DarkTurquoise 0-206-209 + tcl::dict::set TK_colour_map DarkViolet 148-0-211 + tcl::dict::set TK_colour_map "deep pink" 255-20-147 + tcl::dict::set TK_colour_map "deep sky blue" 0-191-255 + tcl::dict::set TK_colour_map DeepPink 255-20-147 + tcl::dict::set TK_colour_map DeepPink1 255-20-147 + tcl::dict::set TK_colour_map DeepPink2 238-18-137 + tcl::dict::set TK_colour_map DeepPink3 205-16-118 + tcl::dict::set TK_colour_map DeepPink4 139-10-80 + tcl::dict::set TK_colour_map DeepSkyBlue 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue1 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue2 0-178-238 + tcl::dict::set TK_colour_map DeepSkyBlue3 0-154-205 + tcl::dict::set TK_colour_map DeepSkyBlue4 0-104-139 + tcl::dict::set TK_colour_map "dim gray" 105-105-105 + tcl::dict::set TK_colour_map "dim grey" 105-105-105 + tcl::dict::set TK_colour_map DimGray 105-105-105 + tcl::dict::set TK_colour_map DimGrey 105-105-105 + tcl::dict::set TK_colour_map "dodger blue" 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue1 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue2 28-134-238 + tcl::dict::set TK_colour_map DodgerBlue3 24-116-205 + tcl::dict::set TK_colour_map DodgerBlue4 16-78-139 + tcl::dict::set TK_colour_map firebrick 178-34-34 + tcl::dict::set TK_colour_map firebrick1 255-48-48 + tcl::dict::set TK_colour_map firebrick2 238-44-44 + tcl::dict::set TK_colour_map firebrick3 205-38-38 + tcl::dict::set TK_colour_map firebrick4 139-26-26 + tcl::dict::set TK_colour_map "floral white" 255-250-240 + tcl::dict::set TK_colour_map FloralWhite 255-250-240 + tcl::dict::set TK_colour_map "forest green" 34-139-34 + tcl::dict::set TK_colour_map ForestGreen 34-139-34 + tcl::dict::set TK_colour_map fuchsia 255-0-255 + tcl::dict::set TK_colour_map gainsboro 220-220-220 + tcl::dict::set TK_colour_map "ghost white" 248-248-255 + tcl::dict::set TK_colour_map GhostWhite 248-248-255 + tcl::dict::set TK_colour_map gold 255-215-0 + tcl::dict::set TK_colour_map gold1 255-215-0 + tcl::dict::set TK_colour_map gold2 238-201-0 + tcl::dict::set TK_colour_map gold3 205-173-0 + tcl::dict::set TK_colour_map gold4 139-117-0 + tcl::dict::set TK_colour_map goldenrod 218-165-32 + tcl::dict::set TK_colour_map goldenrod1 255-193-37 + tcl::dict::set TK_colour_map goldenrod2 238-180-34 + tcl::dict::set TK_colour_map goldenrod3 205-155-29 + tcl::dict::set TK_colour_map goldenrod4 139-105-20 + tcl::dict::set TK_colour_map gray 128-128-128 + tcl::dict::set TK_colour_map gray0 0-0-0 + tcl::dict::set TK_colour_map gray1 3-3-3 + tcl::dict::set TK_colour_map gray2 5-5-5 + tcl::dict::set TK_colour_map gray3 8-8-8 + tcl::dict::set TK_colour_map gray4 10-10-10 + tcl::dict::set TK_colour_map gray5 13-13-13 + tcl::dict::set TK_colour_map gray6 15-15-15 + tcl::dict::set TK_colour_map gray7 18-18-18 + tcl::dict::set TK_colour_map gray8 20-20-20 + tcl::dict::set TK_colour_map gray9 23-23-23 + tcl::dict::set TK_colour_map gray10 26-26-26 + tcl::dict::set TK_colour_map gray11 28-28-28 + tcl::dict::set TK_colour_map gray12 31-31-31 + tcl::dict::set TK_colour_map gray13 33-33-33 + tcl::dict::set TK_colour_map gray14 36-36-36 + tcl::dict::set TK_colour_map gray15 38-38-38 + tcl::dict::set TK_colour_map gray16 41-41-41 + tcl::dict::set TK_colour_map gray17 43-43-43 + tcl::dict::set TK_colour_map gray18 46-46-46 + tcl::dict::set TK_colour_map gray19 48-48-48 + tcl::dict::set TK_colour_map gray20 51-51-51 + tcl::dict::set TK_colour_map gray21 54-54-54 + tcl::dict::set TK_colour_map gray22 56-56-56 + tcl::dict::set TK_colour_map gray23 59-59-59 + tcl::dict::set TK_colour_map gray24 61-61-61 + tcl::dict::set TK_colour_map gray25 64-64-64 + tcl::dict::set TK_colour_map gray26 66-66-66 + tcl::dict::set TK_colour_map gray27 69-69-69 + tcl::dict::set TK_colour_map gray28 71-71-71 + tcl::dict::set TK_colour_map gray29 74-74-74 + tcl::dict::set TK_colour_map gray30 77-77-77 + tcl::dict::set TK_colour_map gray31 79-79-79 + tcl::dict::set TK_colour_map gray32 82-82-82 + tcl::dict::set TK_colour_map gray33 84-84-84 + tcl::dict::set TK_colour_map gray34 87-87-87 + tcl::dict::set TK_colour_map gray35 89-89-89 + tcl::dict::set TK_colour_map gray36 92-92-92 + tcl::dict::set TK_colour_map gray37 94-94-94 + tcl::dict::set TK_colour_map gray38 97-97-97 + tcl::dict::set TK_colour_map gray39 99-99-99 + tcl::dict::set TK_colour_map gray40 102-102-102 + tcl::dict::set TK_colour_map gray41 105-105-105 + tcl::dict::set TK_colour_map gray42 107-107-107 + tcl::dict::set TK_colour_map gray43 110-110-110 + tcl::dict::set TK_colour_map gray44 112-112-112 + tcl::dict::set TK_colour_map gray45 115-115-115 + tcl::dict::set TK_colour_map gray46 117-117-117 + tcl::dict::set TK_colour_map gray47 120-120-120 + tcl::dict::set TK_colour_map gray48 122-122-122 + tcl::dict::set TK_colour_map gray49 125-125-125 + tcl::dict::set TK_colour_map gray50 127-127-127 + tcl::dict::set TK_colour_map gray51 130-130-130 + tcl::dict::set TK_colour_map gray52 133-133-133 + tcl::dict::set TK_colour_map gray53 135-135-135 + tcl::dict::set TK_colour_map gray54 138-138-138 + tcl::dict::set TK_colour_map gray55 140-140-140 + tcl::dict::set TK_colour_map gray56 143-143-143 + tcl::dict::set TK_colour_map gray57 145-145-145 + tcl::dict::set TK_colour_map gray58 148-148-148 + tcl::dict::set TK_colour_map gray59 150-150-150 + tcl::dict::set TK_colour_map gray60 153-153-153 + tcl::dict::set TK_colour_map gray61 156-156-156 + tcl::dict::set TK_colour_map gray62 158-158-158 + tcl::dict::set TK_colour_map gray63 161-161-161 + tcl::dict::set TK_colour_map gray64 163-163-163 + tcl::dict::set TK_colour_map gray65 166-166-166 + tcl::dict::set TK_colour_map gray66 168-168-168 + tcl::dict::set TK_colour_map gray67 171-171-171 + tcl::dict::set TK_colour_map gray68 173-173-173 + tcl::dict::set TK_colour_map gray69 176-176-176 + tcl::dict::set TK_colour_map gray70 179-179-179 + tcl::dict::set TK_colour_map gray71 181-181-181 + tcl::dict::set TK_colour_map gray72 184-184-184 + tcl::dict::set TK_colour_map gray73 186-186-186 + tcl::dict::set TK_colour_map gray74 189-189-189 + tcl::dict::set TK_colour_map gray75 191-191-191 + tcl::dict::set TK_colour_map gray76 194-194-194 + tcl::dict::set TK_colour_map gray77 196-196-196 + tcl::dict::set TK_colour_map gray78 199-199-199 + tcl::dict::set TK_colour_map gray79 201-201-201 + tcl::dict::set TK_colour_map gray80 204-204-204 + tcl::dict::set TK_colour_map gray81 207-207-207 + tcl::dict::set TK_colour_map gray82 209-209-209 + tcl::dict::set TK_colour_map gray83 212-212-212 + tcl::dict::set TK_colour_map gray84 214-214-214 + tcl::dict::set TK_colour_map gray85 217-217-217 + tcl::dict::set TK_colour_map gray86 219-219-219 + tcl::dict::set TK_colour_map gray87 222-222-222 + tcl::dict::set TK_colour_map gray88 224-224-224 + tcl::dict::set TK_colour_map gray89 227-227-227 + tcl::dict::set TK_colour_map gray90 229-229-229 + tcl::dict::set TK_colour_map gray91 232-232-232 + tcl::dict::set TK_colour_map gray92 235-235-235 + tcl::dict::set TK_colour_map gray93 237-237-237 + tcl::dict::set TK_colour_map gray94 240-240-240 + tcl::dict::set TK_colour_map gray95 242-242-242 + tcl::dict::set TK_colour_map gray96 245-245-245 + tcl::dict::set TK_colour_map gray97 247-247-247 + tcl::dict::set TK_colour_map gray98 250-250-250 + tcl::dict::set TK_colour_map gray99 252-252-252 + tcl::dict::set TK_colour_map gray100 255-255-255 + tcl::dict::set TK_colour_map green 0-128-0 + tcl::dict::set TK_colour_map "green yellow" 173-255-47 + tcl::dict::set TK_colour_map green1 0-255-0 + tcl::dict::set TK_colour_map green2 0-238-0 + tcl::dict::set TK_colour_map green3 0-205-0 + tcl::dict::set TK_colour_map green4 0-139-0 + tcl::dict::set TK_colour_map GreenYellow 173-255-47 + tcl::dict::set TK_colour_map grey 128-128-128 + tcl::dict::set TK_colour_map grey0 0-0-0 + tcl::dict::set TK_colour_map grey1 3-3-3 + tcl::dict::set TK_colour_map grey2 5-5-5 + tcl::dict::set TK_colour_map grey3 8-8-8 + tcl::dict::set TK_colour_map grey4 10-10-10 + tcl::dict::set TK_colour_map grey5 13-13-13 + tcl::dict::set TK_colour_map grey6 15-15-15 + tcl::dict::set TK_colour_map grey7 18-18-18 + tcl::dict::set TK_colour_map grey8 20-20-20 + tcl::dict::set TK_colour_map grey9 23-23-23 + tcl::dict::set TK_colour_map grey10 26-26-26 + tcl::dict::set TK_colour_map grey11 28-28-28 + tcl::dict::set TK_colour_map grey12 31-31-31 + tcl::dict::set TK_colour_map grey13 33-33-33 + tcl::dict::set TK_colour_map grey14 36-36-36 + tcl::dict::set TK_colour_map grey15 38-38-38 + tcl::dict::set TK_colour_map grey16 41-41-41 + tcl::dict::set TK_colour_map grey17 43-43-43 + tcl::dict::set TK_colour_map grey18 46-46-46 + tcl::dict::set TK_colour_map grey19 48-48-48 + tcl::dict::set TK_colour_map grey20 51-51-51 + tcl::dict::set TK_colour_map grey21 54-54-54 + tcl::dict::set TK_colour_map grey22 56-56-56 + tcl::dict::set TK_colour_map grey23 59-59-59 + tcl::dict::set TK_colour_map grey24 61-61-61 + tcl::dict::set TK_colour_map grey25 64-64-64 + tcl::dict::set TK_colour_map grey26 66-66-66 + tcl::dict::set TK_colour_map grey27 69-69-69 + tcl::dict::set TK_colour_map grey28 71-71-71 + tcl::dict::set TK_colour_map grey29 74-74-74 + tcl::dict::set TK_colour_map grey30 77-77-77 + tcl::dict::set TK_colour_map grey31 79-79-79 + tcl::dict::set TK_colour_map grey32 82-82-82 + tcl::dict::set TK_colour_map grey33 84-84-84 + tcl::dict::set TK_colour_map grey34 87-87-87 + tcl::dict::set TK_colour_map grey35 89-89-89 + tcl::dict::set TK_colour_map grey36 92-92-92 + tcl::dict::set TK_colour_map grey37 94-94-94 + tcl::dict::set TK_colour_map grey38 97-97-97 + tcl::dict::set TK_colour_map grey39 99-99-99 + tcl::dict::set TK_colour_map grey40 102-102-102 + tcl::dict::set TK_colour_map grey41 105-105-105 + tcl::dict::set TK_colour_map grey42 107-107-107 + tcl::dict::set TK_colour_map grey43 110-110-110 + tcl::dict::set TK_colour_map grey44 112-112-112 + tcl::dict::set TK_colour_map grey45 115-115-115 + tcl::dict::set TK_colour_map grey46 117-117-117 + tcl::dict::set TK_colour_map grey47 120-120-120 + tcl::dict::set TK_colour_map grey48 122-122-122 + tcl::dict::set TK_colour_map grey49 125-125-125 + tcl::dict::set TK_colour_map grey50 127-127-127 + tcl::dict::set TK_colour_map grey51 130-130-130 + tcl::dict::set TK_colour_map grey52 133-133-133 + tcl::dict::set TK_colour_map grey53 135-135-135 + tcl::dict::set TK_colour_map grey54 138-138-138 + tcl::dict::set TK_colour_map grey55 140-140-140 + tcl::dict::set TK_colour_map grey56 143-143-143 + tcl::dict::set TK_colour_map grey57 145-145-145 + tcl::dict::set TK_colour_map grey58 148-148-148 + tcl::dict::set TK_colour_map grey59 150-150-150 + tcl::dict::set TK_colour_map grey60 153-153-153 + tcl::dict::set TK_colour_map grey61 156-156-156 + tcl::dict::set TK_colour_map grey62 158-158-158 + tcl::dict::set TK_colour_map grey63 161-161-161 + tcl::dict::set TK_colour_map grey64 163-163-163 + tcl::dict::set TK_colour_map grey65 166-166-166 + tcl::dict::set TK_colour_map grey66 168-168-168 + tcl::dict::set TK_colour_map grey67 171-171-171 + tcl::dict::set TK_colour_map grey68 173-173-173 + tcl::dict::set TK_colour_map grey69 176-176-176 + tcl::dict::set TK_colour_map grey70 179-179-179 + tcl::dict::set TK_colour_map grey71 181-181-181 + tcl::dict::set TK_colour_map grey72 184-184-184 + tcl::dict::set TK_colour_map grey73 186-186-186 + tcl::dict::set TK_colour_map grey74 189-189-189 + tcl::dict::set TK_colour_map grey75 191-191-191 + tcl::dict::set TK_colour_map grey76 194-194-194 + tcl::dict::set TK_colour_map grey77 196-196-196 + tcl::dict::set TK_colour_map grey78 199-199-199 + tcl::dict::set TK_colour_map grey79 201-201-201 + tcl::dict::set TK_colour_map grey80 204-204-204 + tcl::dict::set TK_colour_map grey81 207-207-207 + tcl::dict::set TK_colour_map grey82 209-209-209 + tcl::dict::set TK_colour_map grey83 212-212-212 + tcl::dict::set TK_colour_map grey84 214-214-214 + tcl::dict::set TK_colour_map grey85 217-217-217 + tcl::dict::set TK_colour_map grey86 219-219-219 + tcl::dict::set TK_colour_map grey87 222-222-222 + tcl::dict::set TK_colour_map grey88 224-224-224 + tcl::dict::set TK_colour_map grey89 227-227-227 + tcl::dict::set TK_colour_map grey90 229-229-229 + tcl::dict::set TK_colour_map grey91 232-232-232 + tcl::dict::set TK_colour_map grey92 235-235-235 + tcl::dict::set TK_colour_map grey93 237-237-237 + tcl::dict::set TK_colour_map grey94 240-240-240 + tcl::dict::set TK_colour_map grey95 242-242-242 + tcl::dict::set TK_colour_map grey96 245-245-245 + tcl::dict::set TK_colour_map grey97 247-247-247 + tcl::dict::set TK_colour_map grey98 250-250-250 + tcl::dict::set TK_colour_map grey99 252-252-252 + tcl::dict::set TK_colour_map grey100 255-255-255 + tcl::dict::set TK_colour_map honeydew 240-255-240 + tcl::dict::set TK_colour_map honeydew1 240-255-240 + tcl::dict::set TK_colour_map honeydew2 224-238-224 + tcl::dict::set TK_colour_map honeydew3 193-205-193 + tcl::dict::set TK_colour_map honeydew4 131-139-131 + tcl::dict::set TK_colour_map "hot pink" 255-105-180 + tcl::dict::set TK_colour_map HotPink 255-105-180 + tcl::dict::set TK_colour_map HotPink1 255-110-180 + tcl::dict::set TK_colour_map HotPink2 238-106-167 + tcl::dict::set TK_colour_map HotPink3 205-96-144 + tcl::dict::set TK_colour_map HotPink4 139-58-98 + tcl::dict::set TK_colour_map "indian red" 205-92-92 + tcl::dict::set TK_colour_map IndianRed 205-92-92 + tcl::dict::set TK_colour_map IndianRed1 255-106-106 + tcl::dict::set TK_colour_map IndianRed2 238-99-99 + tcl::dict::set TK_colour_map IndianRed3 205-85-85 + tcl::dict::set TK_colour_map IndianRed4 139-58-58 + tcl::dict::set TK_colour_map indigo 75-0-130 + tcl::dict::set TK_colour_map ivory 255-255-240 + tcl::dict::set TK_colour_map ivory1 255-255-240 + tcl::dict::set TK_colour_map ivory2 238-238-224 + tcl::dict::set TK_colour_map ivory3 205-205-193 + tcl::dict::set TK_colour_map ivory4 139-139-131 + tcl::dict::set TK_colour_map khaki 240-230-140 + tcl::dict::set TK_colour_map khaki1 255-246-143 + tcl::dict::set TK_colour_map khaki2 238-230-133 + tcl::dict::set TK_colour_map khaki3 205-198-115 + tcl::dict::set TK_colour_map khaki4 139-134-78 + tcl::dict::set TK_colour_map lavender 230-230-250 + tcl::dict::set TK_colour_map "lavender blush" 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush1 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush2 238-224-229 + tcl::dict::set TK_colour_map LavenderBlush3 205-193-197 + tcl::dict::set TK_colour_map LavenderBlush4 139-131-134 + tcl::dict::set TK_colour_map "lawn green" 124-252-0 + tcl::dict::set TK_colour_map LawnGreen 124-252-0 + tcl::dict::set TK_colour_map "lemon chiffon" 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon1 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon2 238-233-191 + tcl::dict::set TK_colour_map LemonChiffon3 205-201-165 + tcl::dict::set TK_colour_map LemonChiffon4 139-137-112 + tcl::dict::set TK_colour_map "light blue" 173-216-230 + tcl::dict::set TK_colour_map "light coral" 240-128-128 + tcl::dict::set TK_colour_map "light cyan" 224-255-255 + tcl::dict::set TK_colour_map "light goldenrod" 238-221-130 + tcl::dict::set TK_colour_map "light goldenrod yellow" 250-250-210 + tcl::dict::set TK_colour_map "light gray" 211-211-211 + tcl::dict::set TK_colour_map "light green" 144-238-144 + tcl::dict::set TK_colour_map "light grey" 211-211-211 + tcl::dict::set TK_colour_map "light pink" 255-182-193 + tcl::dict::set TK_colour_map "light salmon" 255-160-122 + tcl::dict::set TK_colour_map "light sea green" 32-178-170 + tcl::dict::set TK_colour_map "light sky blue" 135-206-250 + tcl::dict::set TK_colour_map "light slate blue" 132-112-255 + tcl::dict::set TK_colour_map "light slate gray" 119-136-153 + tcl::dict::set TK_colour_map "light slate grey" 119-136-153 + tcl::dict::set TK_colour_map "light steel blue" 176-196-222 + tcl::dict::set TK_colour_map "light yellow" 255-255-224 + tcl::dict::set TK_colour_map LightBlue 173-216-230 + tcl::dict::set TK_colour_map LightBlue1 191-239-255 + tcl::dict::set TK_colour_map LightBlue2 178-223-238 + tcl::dict::set TK_colour_map LightBlue3 154-192-205 + tcl::dict::set TK_colour_map LightBlue4 104-131-139 + tcl::dict::set TK_colour_map LightCoral 240-128-128 + tcl::dict::set TK_colour_map LightCyan 224-255-255 + tcl::dict::set TK_colour_map LightCyan1 224-255-255 + tcl::dict::set TK_colour_map LightCyan2 209-238-238 + tcl::dict::set TK_colour_map LightCyan3 180-205-205 + tcl::dict::set TK_colour_map LightCyan4 122-139-139 + tcl::dict::set TK_colour_map LightGoldenrod 238-221-130 + tcl::dict::set TK_colour_map LightGoldenrod1 255-236-139 + tcl::dict::set TK_colour_map LightGoldenrod2 238-220-130 + tcl::dict::set TK_colour_map LightGoldenrod3 205-190-112 + tcl::dict::set TK_colour_map LightGoldenrod4 139-129-76 + tcl::dict::set TK_colour_map LightGoldenrodYellow 250-250-210 + tcl::dict::set TK_colour_map LightGray 211-211-211 + tcl::dict::set TK_colour_map LightGreen 144-238-144 + tcl::dict::set TK_colour_map LightGrey 211-211-211 + tcl::dict::set TK_colour_map LightPink 255-182-193 + tcl::dict::set TK_colour_map LightPink1 255-174-185 + tcl::dict::set TK_colour_map LightPink2 238-162-173 + tcl::dict::set TK_colour_map LightPink3 205-140-149 + tcl::dict::set TK_colour_map LightPink4 139-95-101 + tcl::dict::set TK_colour_map LightSalmon 255-160-122 + tcl::dict::set TK_colour_map LightSalmon1 255-160-122 + tcl::dict::set TK_colour_map LightSalmon2 238-149-114 + tcl::dict::set TK_colour_map LightSalmon3 205-129-98 + tcl::dict::set TK_colour_map LightSalmon4 139-87-66 + tcl::dict::set TK_colour_map LightSeaGreen 32-178-170 + tcl::dict::set TK_colour_map LightSkyBlue 135-206-250 + tcl::dict::set TK_colour_map LightSkyBlue1 176-226-255 + tcl::dict::set TK_colour_map LightSkyBlue2 164-211-238 + tcl::dict::set TK_colour_map LightSkyBlue3 141-182-205 + tcl::dict::set TK_colour_map LightSkyBlue4 96-123-139 + tcl::dict::set TK_colour_map LightSlateBlue 132-112-255 + tcl::dict::set TK_colour_map LightSlateGray 119-136-153 + tcl::dict::set TK_colour_map LightSlateGrey 119-136-153 + tcl::dict::set TK_colour_map LightSteelBlue 176-196-222 + tcl::dict::set TK_colour_map LightSteelBlue1 202-225-255 + tcl::dict::set TK_colour_map LightSteelBlue2 188-210-238 + tcl::dict::set TK_colour_map LightSteelBlue3 162-181-205 + tcl::dict::set TK_colour_map LightSteelBlue4 110-123-139 + tcl::dict::set TK_colour_map LightYellow 255-255-224 + tcl::dict::set TK_colour_map LightYellow1 255-255-224 + tcl::dict::set TK_colour_map LightYellow2 238-238-209 + tcl::dict::set TK_colour_map LightYellow3 205-205-180 + tcl::dict::set TK_colour_map LightYellow4 139-139-122 + tcl::dict::set TK_colour_map lime 0-255-0 + tcl::dict::set TK_colour_map "lime green" 50-205-50 + tcl::dict::set TK_colour_map LimeGreen 50-205-50 + tcl::dict::set TK_colour_map linen 250-240-230 + tcl::dict::set TK_colour_map magenta 255-0-255 + tcl::dict::set TK_colour_map magenta1 255-0-255 + tcl::dict::set TK_colour_map magenta2 238-0-238 + tcl::dict::set TK_colour_map magenta3 205-0-205 + tcl::dict::set TK_colour_map magenta4 139-0-139 + tcl::dict::set TK_colour_map maroon 128-0-0 + tcl::dict::set TK_colour_map maroon1 255-52-179 + tcl::dict::set TK_colour_map maroon2 238-48-167 + tcl::dict::set TK_colour_map maroon3 205-41-144 + tcl::dict::set TK_colour_map maroon4 139-28-98 + tcl::dict::set TK_colour_map "medium aquamarine" 102-205-170 + tcl::dict::set TK_colour_map "medium blue" 0-0-205 + tcl::dict::set TK_colour_map "medium orchid" 186-85-211 + tcl::dict::set TK_colour_map "medium purple" 147-112-219 + tcl::dict::set TK_colour_map "medium sea green" 60-179-113 + tcl::dict::set TK_colour_map "medium slate blue" 123-104-238 + tcl::dict::set TK_colour_map "medium spring green" 0-250-154 + tcl::dict::set TK_colour_map "medium turquoise" 72-209-204 + tcl::dict::set TK_colour_map "medium violet red" 199-21-133 + tcl::dict::set TK_colour_map MediumAquamarine 102-205-170 + tcl::dict::set TK_colour_map MediumBlue 0-0-205 + tcl::dict::set TK_colour_map MediumOrchid 186-85-211 + tcl::dict::set TK_colour_map MediumOrchid1 224-102-255 + tcl::dict::set TK_colour_map MediumOrchid2 209-95-238 + tcl::dict::set TK_colour_map MediumOrchid3 180-82-205 + tcl::dict::set TK_colour_map MediumOrchid4 122-55-139 + tcl::dict::set TK_colour_map MediumPurple 147-112-219 + tcl::dict::set TK_colour_map MediumPurple1 171-130-255 + tcl::dict::set TK_colour_map MediumPurple2 159-121-238 + tcl::dict::set TK_colour_map MediumPurple3 137-104-205 + tcl::dict::set TK_colour_map MediumPurple4 93-71-139 + tcl::dict::set TK_colour_map MediumSeaGreen 60-179-113 + tcl::dict::set TK_colour_map MediumSlateBlue 123-104-238 + tcl::dict::set TK_colour_map MediumSpringGreen 0-250-154 + tcl::dict::set TK_colour_map MediumTurquoise 72-209-204 + tcl::dict::set TK_colour_map MediumVioletRed 199-21-133 + tcl::dict::set TK_colour_map "midnight blue" 25-25-112 + tcl::dict::set TK_colour_map MidnightBlue 25-25-112 + tcl::dict::set TK_colour_map "mint cream" 245-255-250 + tcl::dict::set TK_colour_map MintCream 245-255-250 + tcl::dict::set TK_colour_map "misty rose" 255-228-225 + tcl::dict::set TK_colour_map MistyRose 255-228-225 + tcl::dict::set TK_colour_map MistyRose1 255-228-225 + tcl::dict::set TK_colour_map MistyRose2 238-213-210 + tcl::dict::set TK_colour_map MistyRose3 205-183-181 + tcl::dict::set TK_colour_map MistyRose4 139-125-123 + tcl::dict::set TK_colour_map moccasin 255-228-181 + tcl::dict::set TK_colour_map "navajo white" 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite1 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite2 238-207-161 + tcl::dict::set TK_colour_map NavajoWhite3 205-179-139 + tcl::dict::set TK_colour_map NavajoWhite4 139-121-94 + tcl::dict::set TK_colour_map navy 0-0-128 + tcl::dict::set TK_colour_map "navy blue" 0-0-128 + tcl::dict::set TK_colour_map NavyBlue 0-0-128 + tcl::dict::set TK_colour_map "old lace" 253-245-230 + tcl::dict::set TK_colour_map OldLace 253-245-230 + tcl::dict::set TK_colour_map olive 128-128-0 + tcl::dict::set TK_colour_map "olive drab" 107-142-35 + tcl::dict::set TK_colour_map OliveDrab 107-142-35 + tcl::dict::set TK_colour_map OliveDrab1 192-255-62 + tcl::dict::set TK_colour_map OliveDrab2 179-238-58 + tcl::dict::set TK_colour_map OliveDrab3 154-205-50 + tcl::dict::set TK_colour_map OliveDrab4 105-139-34 + tcl::dict::set TK_colour_map orange 255-165-0 + tcl::dict::set TK_colour_map "orange red" 255-69-0 + tcl::dict::set TK_colour_map orange1 255-165-0 + tcl::dict::set TK_colour_map orange2 238-154-0 + tcl::dict::set TK_colour_map orange3 205-133-0 + tcl::dict::set TK_colour_map orange4 139-90-0 + tcl::dict::set TK_colour_map OrangeRed 255-69-0 + tcl::dict::set TK_colour_map OrangeRed1 255-69-0 + tcl::dict::set TK_colour_map OrangeRed2 238-64-0 + tcl::dict::set TK_colour_map OrangeRed3 205-55-0 + tcl::dict::set TK_colour_map OrangeRed4 139-37-0 + tcl::dict::set TK_colour_map orchid 218-112-214 + tcl::dict::set TK_colour_map orchid1 255-131-250 + tcl::dict::set TK_colour_map orchid2 238-122-233 + tcl::dict::set TK_colour_map orchid3 205-105-201 + tcl::dict::set TK_colour_map orchid4 139-71-137 + tcl::dict::set TK_colour_map "pale goldenrod" 238-232-170 + tcl::dict::set TK_colour_map "pale green" 152-251-152 + tcl::dict::set TK_colour_map "pale turquoise" 175-238-238 + tcl::dict::set TK_colour_map "pale violet red" 219-112-147 + tcl::dict::set TK_colour_map PaleGoldenrod 238-232-170 + tcl::dict::set TK_colour_map PaleGreen 152-251-152 + tcl::dict::set TK_colour_map PaleGreen1 154-255-154 + tcl::dict::set TK_colour_map PaleGreen2 144-238-144 + tcl::dict::set TK_colour_map PaleGreen3 124-205-124 + tcl::dict::set TK_colour_map PaleGreen4 84-139-84 + tcl::dict::set TK_colour_map PaleTurquoise 175-238-238 + tcl::dict::set TK_colour_map PaleTurquoise1 187-255-255 + tcl::dict::set TK_colour_map PaleTurquoise2 174-238-238 + tcl::dict::set TK_colour_map PaleTurquoise3 150-205-205 + tcl::dict::set TK_colour_map PaleTurquoise4 102-139-139 + tcl::dict::set TK_colour_map PaleVioletRed 219-112-147 + tcl::dict::set TK_colour_map PaleVioletRed1 255-130-171 + tcl::dict::set TK_colour_map PaleVioletRed2 238-121-159 + tcl::dict::set TK_colour_map PaleVioletRed3 205-104-127 + tcl::dict::set TK_colour_map PaleVioletRed4 139-71-93 + tcl::dict::set TK_colour_map "papaya whip" 255-239-213 + tcl::dict::set TK_colour_map PapayaWhip 255-239-213 + tcl::dict::set TK_colour_map "peach puff" 255-218-185 + tcl::dict::set TK_colour_map PeachPuff 255-218-185 + tcl::dict::set TK_colour_map PeachPuff1 255-218-185 + tcl::dict::set TK_colour_map PeachPuff2 238-203-173 + tcl::dict::set TK_colour_map PeachPuff3 205-175-149 + tcl::dict::set TK_colour_map PeachPuff4 139-119-101 + tcl::dict::set TK_colour_map peru 205-133-63 + tcl::dict::set TK_colour_map pink 255-192-203 + tcl::dict::set TK_colour_map pink1 255-181-197 + tcl::dict::set TK_colour_map pink2 238-169-184 + tcl::dict::set TK_colour_map pink3 205-145-158 + tcl::dict::set TK_colour_map pink4 139-99-108 + tcl::dict::set TK_colour_map plum 221-160-221 + tcl::dict::set TK_colour_map plum1 255-187-255 + tcl::dict::set TK_colour_map plum2 238-174-238 + tcl::dict::set TK_colour_map plum3 205-150-205 + tcl::dict::set TK_colour_map plum4 139-102-139 + tcl::dict::set TK_colour_map "powder blue" 176-224-230 + tcl::dict::set TK_colour_map PowderBlue 176-224-230 + tcl::dict::set TK_colour_map purple 128-0-128 + tcl::dict::set TK_colour_map purple1 155-48-255 + tcl::dict::set TK_colour_map purple2 145-44-238 + tcl::dict::set TK_colour_map purple3 125-38-205 + tcl::dict::set TK_colour_map purple4 85-26-139 + tcl::dict::set TK_colour_map red 255-0-0 + tcl::dict::set TK_colour_map red1 255-0-0 + tcl::dict::set TK_colour_map red2 238-0-0 + tcl::dict::set TK_colour_map red3 205-0-0 + tcl::dict::set TK_colour_map red4 139-0-0 + tcl::dict::set TK_colour_map "rosy brown" 188-143-143 + tcl::dict::set TK_colour_map RosyBrown 188-143-143 + tcl::dict::set TK_colour_map RosyBrown1 255-193-193 + tcl::dict::set TK_colour_map RosyBrown2 238-180-180 + tcl::dict::set TK_colour_map RosyBrown3 205-155-155 + tcl::dict::set TK_colour_map RosyBrown4 139-105-105 + tcl::dict::set TK_colour_map "royal blue" 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue1 72-118-255 + tcl::dict::set TK_colour_map RoyalBlue2 67-110-238 + tcl::dict::set TK_colour_map RoyalBlue3 58-95-205 + tcl::dict::set TK_colour_map RoyalBlue4 39-64-139 + tcl::dict::set TK_colour_map "saddle brown" 139-69-19 + tcl::dict::set TK_colour_map SaddleBrown 139-69-19 + tcl::dict::set TK_colour_map salmon 250-128-114 + tcl::dict::set TK_colour_map salmon1 255-140-105 + tcl::dict::set TK_colour_map salmon2 238-130-98 + tcl::dict::set TK_colour_map salmon3 205-112-84 + tcl::dict::set TK_colour_map salmon4 139-76-57 + tcl::dict::set TK_colour_map "sandy brown" 244-164-96 + tcl::dict::set TK_colour_map SandyBrown 244-164-96 + tcl::dict::set TK_colour_map "sea green" 46-139-87 + tcl::dict::set TK_colour_map SeaGreen 46-139-87 + tcl::dict::set TK_colour_map SeaGreen1 84-255-159 + tcl::dict::set TK_colour_map SeaGreen2 78-238-148 + tcl::dict::set TK_colour_map SeaGreen3 67-205-128 + tcl::dict::set TK_colour_map SeaGreen4 46-139-87 + tcl::dict::set TK_colour_map seashell 255-245-238 + tcl::dict::set TK_colour_map seashell1 255-245-238 + tcl::dict::set TK_colour_map seashell2 238-229-222 + tcl::dict::set TK_colour_map seashell3 205-197-191 + tcl::dict::set TK_colour_map seashell4 139-134-130 + tcl::dict::set TK_colour_map sienna 160-82-45 + tcl::dict::set TK_colour_map sienna1 255-130-71 + tcl::dict::set TK_colour_map sienna2 238-121-66 + tcl::dict::set TK_colour_map sienna3 205-104-57 + tcl::dict::set TK_colour_map sienna4 139-71-38 + tcl::dict::set TK_colour_map silver 192-192-192 + tcl::dict::set TK_colour_map "sky blue" 135-206-235 + tcl::dict::set TK_colour_map SkyBlue 135-206-235 + tcl::dict::set TK_colour_map SkyBlue1 135-206-255 + tcl::dict::set TK_colour_map SkyBlue2 126-192-238 + tcl::dict::set TK_colour_map SkyBlue3 108-166-205 + tcl::dict::set TK_colour_map SkyBlue4 74-112-139 + tcl::dict::set TK_colour_map "slate blue" 106-90-205 + tcl::dict::set TK_colour_map "slate gray" 112-128-144 + tcl::dict::set TK_colour_map "slate grey" 112-128-144 + tcl::dict::set TK_colour_map SlateBlue 106-90-205 + tcl::dict::set TK_colour_map SlateBlue1 131-111-255 + tcl::dict::set TK_colour_map SlateBlue2 122-103-238 + tcl::dict::set TK_colour_map SlateBlue3 105-89-205 + tcl::dict::set TK_colour_map SlateBlue4 71-60-139 + tcl::dict::set TK_colour_map SlateGray 112-128-144 + tcl::dict::set TK_colour_map SlateGray1 198-226-255 + tcl::dict::set TK_colour_map SlateGray2 185-211-238 + tcl::dict::set TK_colour_map SlateGray3 159-182-205 + tcl::dict::set TK_colour_map SlateGray4 108-123-139 + tcl::dict::set TK_colour_map SlateGrey 112-128-144 + tcl::dict::set TK_colour_map snow 255-250-250 + tcl::dict::set TK_colour_map snow1 255-250-250 + tcl::dict::set TK_colour_map snow2 238-233-233 + tcl::dict::set TK_colour_map snow3 205-201-201 + tcl::dict::set TK_colour_map snow4 139-137-137 + tcl::dict::set TK_colour_map "spring green" 0-255-127 + tcl::dict::set TK_colour_map SpringGreen 0-255-127 + tcl::dict::set TK_colour_map SpringGreen1 0-255-127 + tcl::dict::set TK_colour_map SpringGreen2 0-238-118 + tcl::dict::set TK_colour_map SpringGreen3 0-205-102 + tcl::dict::set TK_colour_map SpringGreen4 0-139-69 + tcl::dict::set TK_colour_map "steel blue" 70-130-180 + tcl::dict::set TK_colour_map SteelBlue 70-130-180 + tcl::dict::set TK_colour_map SteelBlue1 99-184-255 + tcl::dict::set TK_colour_map SteelBlue2 92-172-238 + tcl::dict::set TK_colour_map SteelBlue3 79-148-205 + tcl::dict::set TK_colour_map SteelBlue4 54-100-139 + tcl::dict::set TK_colour_map tan 210-180-140 + tcl::dict::set TK_colour_map tan1 255-165-79 + tcl::dict::set TK_colour_map tan2 238-154-73 + tcl::dict::set TK_colour_map tan3 205-133-63 + tcl::dict::set TK_colour_map tan4 139-90-43 + tcl::dict::set TK_colour_map teal 0-128-128 + tcl::dict::set TK_colour_map thistle 216-191-216 + tcl::dict::set TK_colour_map thistle1 255-225-255 + tcl::dict::set TK_colour_map thistle2 238-210-238 + tcl::dict::set TK_colour_map thistle3 205-181-205 + tcl::dict::set TK_colour_map thistle4 139-123-139 + tcl::dict::set TK_colour_map tomato 255-99-71 + tcl::dict::set TK_colour_map tomato1 255-99-71 + tcl::dict::set TK_colour_map tomato2 238-92-66 + tcl::dict::set TK_colour_map tomato3 205-79-57 + tcl::dict::set TK_colour_map tomato4 139-54-38 + tcl::dict::set TK_colour_map turquoise 64-224-208 + tcl::dict::set TK_colour_map turquoise1 0-245-255 + tcl::dict::set TK_colour_map turquoise2 0-229-238 + tcl::dict::set TK_colour_map turquoise3 0-197-205 + tcl::dict::set TK_colour_map turquoise4 0-134-139 + tcl::dict::set TK_colour_map violet 238-130-238 + tcl::dict::set TK_colour_map "violet red" 208-32-144 + tcl::dict::set TK_colour_map VioletRed 208-32-144 + tcl::dict::set TK_colour_map VioletRed1 255-62-150 + tcl::dict::set TK_colour_map VioletRed2 238-58-140 + tcl::dict::set TK_colour_map VioletRed3 205-50-120 + tcl::dict::set TK_colour_map VioletRed4 139-34-82 + tcl::dict::set TK_colour_map wheat 245-222-179 + tcl::dict::set TK_colour_map wheat1 255-231-186 + tcl::dict::set TK_colour_map wheat2 238-216-174 + tcl::dict::set TK_colour_map wheat3 205-186-150 + tcl::dict::set TK_colour_map wheat4 139-126-102 + tcl::dict::set TK_colour_map white 255-255-255 + tcl::dict::set TK_colour_map "white smoke" 245-245-245 + tcl::dict::set TK_colour_map WhiteSmoke 245-245-245 + tcl::dict::set TK_colour_map yellow 255-255-0 + tcl::dict::set TK_colour_map "yellow green" 154-205-50 + tcl::dict::set TK_colour_map yellow1 255-255-0 + tcl::dict::set TK_colour_map yellow2 238-238-0 + tcl::dict::set TK_colour_map yellow3 205-205-0 + tcl::dict::set TK_colour_map yellow4 139-139-0 + tcl::dict::set TK_colour_map YellowGreen 154-205-50 + + variable TK_colour_map_lookup ;#same dict but with lower-case versions added + set TK_colour_map_lookup $TK_colour_map + dict for {key val} $TK_colour_map { + dict set TK_colour_map_lookup [tcl::string::tolower $key] $val ;#no need to test if already present - just set. + } + + variable TK_colour_map_reverse [dict create] + dict for {key val} $TK_colour_map { + dict lappend TK_colour_map_reverse $val $key + } + + #using same order as inital colour map + variable TK_colour_map_merge [dict create] + set seen_names [dict create] + dict for {key val} $TK_colour_map { + if {[dict exists $seen_names $key]} { + continue + } + set allnames [dict get $TK_colour_map_reverse $val] + set names [list] + foreach n $allnames { + if {$n ne $key} { + lappend names $n + } + } + dict set TK_colour_map_merge $key [dict create colour $val names $names] + foreach n $names { + dict set seen_names $n 1 + } + } + unset seen_names + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval ::punk::ansi::colourmap::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +# variable PUNKARGS +# variable PUNKARGS_aliases +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::ansi::colourmap +} +# ----------------------------------------------------------------------------- + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::ansi::colourmap [tcl::namespace::eval ::punk::ansi::colourmap { + variable pkg ::punk::ansi::colourmap + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/bootsupport/modules/punk/args-0.1.9.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.tm similarity index 61% rename from src/bootsupport/modules/punk/args-0.1.9.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.tm index e64f2d54..7710fa00 100644 --- a/src/bootsupport/modules/punk/args-0.1.9.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.1.9 +# Application punk::args 0.2 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.9] +#[manpage_begin punkshell_module_punk::args 0 0.2] #[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 --}] @@ -268,6 +268,7 @@ tcl::namespace::eval punk::args::register { #[list_end] [comment {--- end definitions namespace punk::args::register ---}] } +tcl::namespace::eval ::punk::args {} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Base namespace @@ -376,17 +377,28 @@ tcl::namespace::eval punk::args { %B%@id%N% ?opt val...? directive-options: -id %B%@cmd%N% ?opt val...? - directive-options: -name -help + directive-options: -name + -summary + -help %B%@leaders%N% ?opt val...? (used for leading args that come before switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) + directive-options: + -min -max (min and max number of leaders) + -unnamed (allow unnamed positional leaders) + -takewhenargsmodulo (assign args to leaders based on modulo + of total number of args. If value is not supplied (or < 2) then + leaders are assigned based on whether configured opts are + encountered, and whether the min number of leaders and values + can be satisfied. In this case optional leaders are assigned if + the type of the argument can be matched.) + (also accepts options as defaults for subsequent leader definitions) %B%@opts%N% ?opt val...? directive-options: -any|-arbitrary + (also accepts options as defaults for subsequent flag definitions) %B%@values%N% ?opt val...? (used for trailing args that come after switches/opts) directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) + (also accepts options as defaults for subsequent value definitions) %B%@form%N% ?opt val...? (used for commands with multiple forms) directive-options: -form -synopsis @@ -397,6 +409,8 @@ tcl::namespace::eval punk::args { -body (override autogenerated arg info for form) %B%@doc%N% ?opt val...? directive-options: -name -url + %B%@examples%N% ?opt val...? + directive-options: -help %B%@seealso%N% ?opt val...? directive-options: -name -url (for footer - unimplemented) @@ -427,9 +441,35 @@ tcl::namespace::eval punk::args { and trailing values also take spec-options: -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. + A typenamelist represents a multi-value clause where each + value must match the specified type in order. This is not + valid for flags - which can only take a single value. + + typename and entries in typenamelist can take 2 forms: + 1) basic form: elements of llength 1 such as a simple type, + or a pipe-delimited set of type-alternates. + e.g for a single typename: + -type int, -type int|char, -type int|literal(abc) + e.g for a typenamelist + -type {int double}, -type {int|char double} + 2) special form: elements of variable length + e.g for a single typename: + -type {{literal |}} + -type {{literal | | literal (}} + e.g for a typenamelist + -type {{literal |} {stringstartswith abc | int}} + The 2 forms can be mixed: + -type {{literal |} {stringstartswith a|c | int} literal(xyz)|int} + + Defaults to string. If no other restrictions + are required, choosing -type any does the least validation. recognised types: + any + (unvalidated - accepts anything) + none + (used for flags/switches only. Indicates this is + a 'solo' flag ie accepts no value) + Not valid as a member of a clause's typenamelist. int integer number @@ -452,11 +492,9 @@ tcl::namespace::eval punk::args { string (also any of the 'string is' types such as xdigit, graph, punct, lower etc) - any - (unvalidated - accepts anything) - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) + -type string on its own does not need validation, + but still checks for string-related restrictions + such as regexprefail, & minsize literal() (exact match for string) @@ -464,6 +502,9 @@ tcl::namespace::eval punk::args { (prefix match for string, other literal and literalprefix entries specified as alternates using | are used in the calculation) + stringstartswith() + (value must match glob *) + The value of string must not contain pipe char '|' Note that types can be combined with | to indicate an 'or' operation @@ -581,7 +622,8 @@ tcl::namespace::eval punk::args { inner loops in more performance-sensitive code. " @values -min 1 -max -1 - text -type string -multiple 1 -help\ + #text should be a well-formed Tcl list + text -type list -multiple 1 -help\ {Block(s) of text representing the argument definition for a command. At least one must be supplied. If multiple, they are joined together with \n. Using multiple text arguments may be useful to mix curly-braced and double-quoted @@ -622,7 +664,7 @@ tcl::namespace::eval punk::args { proc New_command_form {name} { #probably faster to inline a literal dict create in the proc than to use a namespace variable set leaderdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -637,7 +679,7 @@ tcl::namespace::eval punk::args { -ensembleparameter 0\ ] set optdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 1\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -650,9 +692,13 @@ tcl::namespace::eval punk::args { -regexprepass {}\ -validationtransform {}\ -prefix 1\ + -parsekey ""\ + -group ""\ ] + #parsekey is name of argument to use as a key in punk::args::parse result dicts + set valdirective_defaults [tcl::dict::create\ - -type string\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -677,6 +723,7 @@ tcl::namespace::eval punk::args { LEADER_NAMES [list]\ LEADER_MIN ""\ LEADER_MAX ""\ + LEADER_TAKEWHENARGSMODULO 0\ LEADER_UNNAMED false\ LEADERSPEC_DEFAULTS $leaderdirective_defaults\ LEADER_CHECKS_DEFAULTS {}\ @@ -689,6 +736,7 @@ tcl::namespace::eval punk::args { OPT_SOLOS {}\ OPTSPEC_DEFAULTS $optdirective_defaults\ OPT_CHECKS_DEFAULTS {}\ + OPT_GROUPS {}\ VAL_DEFAULTS [tcl::dict::create]\ VAL_REQUIRED [list]\ VAL_NAMES [list]\ @@ -1014,6 +1062,7 @@ tcl::namespace::eval punk::args { #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table set seealso_info {} set keywords_info {} + set examples_info {} ###set leader_min 0 ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit #set leader_max "" @@ -1145,6 +1194,8 @@ tcl::namespace::eval punk::args { } } default { + #NOTE - this is switch arm for the literal "default" (@default) - not the default arm of the switch block! + #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) @@ -1186,7 +1237,29 @@ tcl::namespace::eval punk::args { # arity system ? #handle multiple parsing styles based on arities and keyword positions (and/or flags?) #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each + + # @form "-synopsis" is optional - and only exists in case the user really wants + # to display something different. The system should generate consistent synopses + # with appropriate italics/bracketing etc. + # For manual -synopsis - features such as italics must be manually added. + + #spitballing.. + #The punk::args parser should generally be able to determine the appropriate form based + #on supplied arguments, e.g automatically using argument counts and matching literals. + #We may need to support some hints for forcing more efficient -form discriminators + # + # e.g compare with -takewhenargsmodulo that is available on @leaders + + #the -arities idea below is a rough one; potentially something to consider.. but + #we want to be able to support command completion.. and things like literals should probably + #take preference for partially typed commands.. as flipping to other forms based on argcount + #could be confusing. Need to match partial command to closest form automatically but allow + #user to lock in a form interactively and see mismatches (?) + #Probably the arity-ranges of a form are best calculated automatically rather than explicitly, + #otherwise we have a strong potential for misdefinition.. (conflict with defined leaders,opts,values) + #The way forward might be to calculate some 'arity' structure from the forms to aid in form-discrimination at arg parse time. + #(this is currently covered in some ways by the LEADER_MIN,LEADER_MAX,OPT_MIN,OPT_MAX,VAL_MIN,VAL_MAX members of the FORMS dict.) + # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ # -arities { # 2 @@ -1214,7 +1287,6 @@ tcl::namespace::eval punk::args { # } #todo - #can we generate a form synopsis if -synopsis not supplied? #form id can be list of ints|names?, or * if {[dict exists $at_specs -form]} { @@ -1232,6 +1304,9 @@ tcl::namespace::eval punk::args { } cmd { #allow arbitrary - review + #e.g -name + # -summary + # -help set cmd_info [dict merge $cmd_info $at_specs] } doc { @@ -1289,6 +1364,37 @@ tcl::namespace::eval punk::args { #} tcl::dict::set tmp_optspec_defaults -type $v } + -parsekey { + tcl::dict::set tmp_optspec_defaults -parsekey $v + + } + -group { + tcl::dict::set tmp_optspec_defaults -group $v + if {$v ne "" && ![tcl::dict::exists $F $fid OPT_GROUPS $v]} { + tcl::dict::set F $fid OPT_GROUPS $v {-parsekey {} -help {}} + } + if {$v ne ""} { + if {[tcl::dict::exists $at_specs -parsekey]} { + tcl::dict::set F $fid OPT_GROUPS $v -parsekey [tcl::dict::get $at_specs -parsekey] + } + } + } + -grouphelp { + if {![tcl::dict::exists $at_specs -group]} { + error "punk::args::resolve Bad @opt line. -group entry is required if -grouphelp is being configured. @id:$DEF_definition_id" + } + set g [tcl::dict::get $at_specs -group] + if {$g eq ""} { + error "punk::args::resolve Bad @opt line. -group non-empty value is required if -grouphelp is being configured. @id:$DEF_definition_id" + } + set groupdict [tcl::dict::get $F $fid OPT_GROUPS] + #set helprecords [tcl::dict::get $F $fid OPT_GROUPS_HELP] + if {![tcl::dict::exists $groupdict $g]} { + tcl::dict::set F $fid OPT_GROUPS $g [dict create -parsekey {} -help $v] + } else { + tcl::dict::set F $fid OPT_GROUPS $g -help $v + } + } -range { if {[dict exists $at_specs -type]} { set tp [dict get $at_specs -type] @@ -1333,7 +1439,8 @@ tcl::namespace::eval punk::args { tcl::dict::set tmp_optspec_defaults $k $v } default { - set known { -any -anyopts -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ + set known { -parsekey -group -grouphelp\ + -any -anyopts -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ -type -range -typeranges -default -typedefaults -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase\ -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ @@ -1376,6 +1483,9 @@ tcl::namespace::eval punk::args { } dict set F $fid LEADER_MAX $v } + -takewhenargsmodulo { + dict set F $fid LEADER_TAKEWHENARGSMODULO $v + } -choiceprefix - -choicerestricted { if {![string is boolean -strict $v]} { @@ -1497,7 +1607,7 @@ tcl::namespace::eval punk::args { -min - -minvalues { if {$v < 0} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is 0. got $v @id:$DEF_definition_id" } #set val_min $v dict set F $fid VAL_MIN $v @@ -1505,7 +1615,7 @@ tcl::namespace::eval punk::args { -max - -maxvalues { if {$v < -1} { - error "punk::args::resolve - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" } #set val_max $v dict set F $fid VAL_MAX $v @@ -1611,8 +1721,11 @@ tcl::namespace::eval punk::args { #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? set keywords_info [dict merge $keywords_info $at_specs] } + examples { + set examples_info [dict merge $examples_info $at_specs] + } default { - error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @examples @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" } } #record_type directive @@ -1717,8 +1830,10 @@ tcl::namespace::eval punk::args { } } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + ##set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] tcl::dict::set argdef_values -ARGTYPE option + #set all_choices [_resolve_get_record_choices] foreach fid $record_form_ids { if {[dict get $F $fid argspace] eq "leaders"} { @@ -1727,6 +1842,7 @@ tcl::namespace::eval punk::args { error "punk::args::resolve - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" } set record_type option + dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] } @@ -1783,7 +1899,7 @@ tcl::namespace::eval punk::args { } - #assert - we only get here if it is a value or flag specification line. + #assert - we only get here if it is a leader, value or flag specification line. #assert argdef_values has been set to the value of record_values foreach fid $record_form_ids { @@ -1801,98 +1917,88 @@ tcl::namespace::eval punk::args { foreach {spec specval} $argdef_values { #literal-key switch - bytecompiled to jumpTable switch -- $spec { - -form { - - } + -form {} -type { #todo - could be a list e.g {any int literal(Test)} #case must be preserved in literal bracketed part set typelist [list] foreach typespec $specval { - set lc_typespec [tcl::string::tolower $typespec] - if {[string match {\?*\?} $lc_typespec]} { - set lc_type [string range $lc_typespec 1 end-1] + if {[string match {\?*\?} $typespec]} { + set tspec [string range $typespec 1 end-1] set optional_clausemember true } else { - set lc_type $lc_typespec + set tspec $typespec set optional_clausemember false } - #normalize here so we don't have to test during actual args parsing in main function - set normtype "" ;#assert - should be overridden in all branches of switch - switch -- $lc_type { - int - integer { - set normtype int - } - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - set normtype double - } - bool - boolean { - set normtype bool - } - char - character { - set normtype char - } - dict - dictionary { - set normtype dict - } - index - indexexpression { - set normtype indexexpression - } - "" - none - solo { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $specval] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - set normtype none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + set type_alternatives [_split_type_expression $tspec] + set normlist [list] + foreach alt $type_alternatives { + set firstword [lindex $alt 0] + set lc_firstword [tcl::string::tolower $firstword] + #normalize here so we don't have to test during actual args parsing in main function + set normtype "" ;#assert - should be overridden in all branches of switch + switch -- $lc_firstword { + int - integer {set normtype int} + double - float { + #review - user may wish to preserve 'float' in help display - consider how best to implement + set normtype double + } + bool - boolean {set normtype bool} + char - character {set normtype char} + dict - dictionary {set normtype dict} + index - indexexpression {set normtype indexexpression} + "" - none - solo { + if {$is_opt} { + #review - are we allowing clauses for flags? + #e.g {-flag -type {int int}} + #this isn't very tcl like, where we'd normally mark the flag with -multiple true and + # instead require calling as: -flag -flag + #It seems this is a reasonably rare/unlikely requirement in most commandline tools. + + if {[llength $specval] > 1} { + #makes no sense to have 'none' in a clause + error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" + } + #tcl::dict::set spec_merged -type none + set normtype none + if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { + tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + } + } else { + #solo only valid for flags + error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } else { - #solo only valid for flags - error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } - any - anything { - set normtype any - } - ansi - ansistring { - set normtype ansistring - } - string - globstring { - set normtype $lc_type - } - literal { - if {$is_opt} { - error "punk::args::resolve - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" + any - anything {set normtype any} + ansi - ansistring {set normtype ansistring} + string - globstring {set normtype $lc_firstword} + literal { + #value was split out by _split_type_expression + set normtype literal([lindex $alt 1]) } - #value is the name of the argument - set normtype literal - } - default { - if {[string match literal* $lc_type]} { - #typespec may or may not be of form ?xxx? - set literal_tail [string range [string trim $typespec ?] 7 end] - set normtype literal$literal_tail - } else { + literalprefix { + set normtype literalprefix([lindex $alt 1]) + } + stringstartswith { + set normtype stringstartswith([lindex $alt 1]) + } + stringendswith { + set normtype stringendswith([lindex $alt 1]) + } + default { #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - set normtype $lc_type + #todo + set normtype $alt } } + lappend normlist $normtype } + set norms [join $normlist |] if {$optional_clausemember} { - lappend typelist ?$normtype? + lappend typelist ?$norms? } else { - lappend typelist $normtype + lappend typelist $norms } } tcl::dict::set spec_merged -type $typelist @@ -1904,6 +2010,9 @@ tcl::namespace::eval punk::args { } tcl::dict::set spec_merged -typesynopsis $specval } + -parsekey - -group { + tcl::dict::set spec_merged -typesynopsis $specval + } -solo - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -choicelabels - -choiceinfo - @@ -2001,8 +2110,12 @@ tcl::namespace::eval punk::args { } } } else { - set known_argopts [list -form -type -range -typeranges\ - -default -typedefaults -minsize -maxsize -choices -choicegroups\ + set known_argopts [list\ + -form -type\ + -parsekey -group\ + -range -typeranges\ + -default -typedefaults\ + -minsize -maxsize -choices -choicegroups\ -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ -regexprepass -regexprefail -regexprefailmsg -validationtransform\ @@ -2017,7 +2130,7 @@ tcl::namespace::eval punk::args { if {$is_opt} { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize if {$argname eq "--"} { #force -type none - in case no -type was specified and @opts -type is some other default such as string tcl::dict::set spec_merged -type none @@ -2027,7 +2140,7 @@ tcl::namespace::eval punk::args { } } else { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize } tcl::dict::set F $fid ARG_INFO $argname $spec_merged #review existence of -default overriding -optional @@ -2053,13 +2166,16 @@ tcl::namespace::eval punk::args { if {[tcl::dict::exists $spec_merged -default]} { if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] + #JJJ + set parsekey [dict get $F $fid ARG_INFO $argname -default] + if {$parsekey eq ""} { + set parsekey $argname + } tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } else { if {[dict get $F $fid argspace] eq "leaders"} { tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } } @@ -2161,6 +2277,8 @@ tcl::namespace::eval punk::args { doc_info $doc_info\ package_info $package_info\ seealso_info $seealso_info\ + keywords_info $keywords_info\ + examples_info $examples_info\ id_info $id_info\ FORMS $F\ form_names [dict keys $F]\ @@ -2191,9 +2309,9 @@ tcl::namespace::eval punk::args { namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} + variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @examples @formdisplay @seealso @leaders @opts @values leaders opts values} variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} + directives {@id @package @cmd @ref @doc @examples @formdisplay @seealso} argumenttypes {leaders opts values} remaining_defaults {@leaders @opts @values} } @@ -2400,7 +2518,7 @@ tcl::namespace::eval punk::args { dict set resultdict @id [list -id [dict get $specdict id]] } } - foreach directive {@package @cmd @doc @seealso} { + foreach directive {@package @cmd @doc @examples @seealso} { set dshort [string range $directive 1 end] if {"$directive" in $included_directives} { if {[dict exists $opt_override $directive]} { @@ -2414,6 +2532,7 @@ tcl::namespace::eval punk::args { } #todo @formdisplay + #todo @ref ? #output ordered by leader, option, value @@ -2465,7 +2584,7 @@ tcl::namespace::eval punk::args { } } } - @package - @cmd - @doc - @seealso { + @package - @cmd - @doc - @examples - @seealso { if {"$type" in $included_directives} { set tp [string range $type 1 end] ;# @package -> package if {[dict exists $opt_override $type]} { @@ -2647,6 +2766,10 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + proc aliases {} { + variable aliases + punk::lib::showdict $aliases + } proc set_alias {alias id} { variable aliases dict set aliases $alias $id @@ -2897,7 +3020,8 @@ tcl::namespace::eval punk::args { set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] #puts "-->$cmdinfo" #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { + set maxloop 10 ;#failsafe + while {[string last \n $cmdinfo] >= 1 && $maxloop > -1} { #looks like a script - haven't gone up far enough? #(e.g patternpunk oo system: >punk . poses -invalidoption) incr call_level -1 @@ -2919,6 +3043,7 @@ tcl::namespace::eval punk::args { break } } + incr maxloop -1 } set caller [regexp -inline {\S+} $cmdinfo] if {$caller eq "namespace"} { @@ -3007,62 +3132,81 @@ tcl::namespace::eval punk::args { "Ordinal index or name of command form" }] ] - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } + variable arg_error_CLR array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] variable arg_error_CLR_nocolour array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" variable arg_error_CLR_info array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] variable arg_error_CLR_error array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] + + proc _argerror_load_colours {{forcereload 0}} { + variable arg_error_CLR + #todo - option for reload/retry? + if {!$forcereload && [array size arg_error_CLR] > 0} { + return + } + + if {[catch {package require punk::ansi} errMsg]} { + puts stderr "punk::args FAILED to load punk::ansi\n$errMsg" + proc ::punk::args::a {args} {} + proc ::punk::args::a+ {args} {} + } else { + namespace eval ::punk::args { + namespace import ::punk::ansi::a ::punk::ansi::a+ + } + } + #array set arg_error_CLR {} + set arg_error_CLR(testsinglecolour) [a+ yellow] ;#A single SGR colour to test current colour on|off state (empty string vs some result - used to determine if forcereload required) + set arg_error_CLR(errormsg) [a+ brightred] + set arg_error_CLR(title) "" + set arg_error_CLR(check) [a+ brightgreen] + set arg_error_CLR(solo) [a+ brightcyan] + set arg_error_CLR(choiceprefix) [a+ underline] + set arg_error_CLR(badarg) [a+ brightred] + set arg_error_CLR(goodarg) [a+ green strike] + set arg_error_CLR(goodchoice) [a+ reverse] + set arg_error_CLR(linebase_header) [a+ white] + set arg_error_CLR(cmdname) [a+ brightwhite] + set arg_error_CLR(groupname) [a+ bold] + set arg_error_CLR(ansiborder) [a+ bold] + set arg_error_CLR(ansibase_header) [a+ bold] + set arg_error_CLR(ansibase_body) [a+ white] + variable arg_error_CLR_nocolour + #array set arg_error_CLR_nocolour {} + set arg_error_CLR_nocolour(errormsg) [a+ bold] + set arg_error_CLR_nocolour(title) [a+ bold] + set arg_error_CLR_nocolour(check) "" + set arg_error_CLR_nocolour(solo) "" + set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment + set arg_error_CLR_nocolour(goodarg) [a+ strike] + set arg_error_CLR_nocolour(cmdname) [a+ bold] + set arg_error_CLR_nocolour(linebase_header) "" + set arg_error_CLR_nocolour(linebase) "" + set arg_error_CLR_nocolour(ansibase_body) "" + variable arg_error_CLR_info + #array set arg_error_CLR_info {} + set arg_error_CLR_info(errormsg) [a+ brightred bold] + set arg_error_CLR_info(title) [a+ brightyellow bold] + set arg_error_CLR_info(check) [a+ brightgreen bold] + set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_info(groupname) [a+ cyan bold] + set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] + set arg_error_CLR_info(ansibase_header) [a+ cyan] + set arg_error_CLR_info(ansibase_body) [a+ white] + variable arg_error_CLR_error + #array set arg_error_CLR_error {} + set arg_error_CLR_error(errormsg) [a+ brightred bold] + set arg_error_CLR_error(title) [a+ brightcyan bold] + set arg_error_CLR_error(check) [a+ brightgreen bold] + set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_error(groupname) [a+ cyan bold] + set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] + set arg_error_CLR_error(ansibase_header) [a+ yellow] + set arg_error_CLR_error(ansibase_body) [a+ white] + } #bas ic recursion blocker @@ -3104,7 +3248,21 @@ tcl::namespace::eval punk::args { error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" } + #set arg_error_CLR(testsinglecolour) [a+ brightred] + upvar ::punk::args::arg_error_CLR 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)]} { + set terminal_colour_is_on [expr {[string length [a+ yellow]]}] + set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + if {$terminal_colour_is_on ^ $error_colour_is_on} { + #results differ + set forcereload 1 + } + } + _argerror_load_colours $forcereload + if {[llength $args] %2 != 0} { + set arg_error_isrunning 0 error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" } @@ -3113,7 +3271,12 @@ 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] + #----------------------- + #todo!! make changeable from config file + #JJJ 2025-07-16 set returntype table ;#table as string + #set returntype string + #---------------------- set as_error 1 ;#usual case is to raise an error set scheme error set form 0 @@ -3197,12 +3360,11 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR switch -- $scheme { nocolour { variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour + array set CLR [array get arg_error_CLR_nocolour] } info { variable arg_error_CLR_info @@ -3249,11 +3411,12 @@ tcl::namespace::eval punk::args { append errmsg \n } } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] + set cmdname [Dict_getdef $spec_dict cmd_info -name ""] + set cmdsummary [Dict_getdef $spec_dict cmd_info -summary ""] + set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] + set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] + set docurl [Dict_getdef $spec_dict doc_info -url ""] #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] @@ -3290,17 +3453,19 @@ tcl::namespace::eval punk::args { set docurl_display "" } #synopsis - set synopsis "" + set synopsis "# [Dict_getdef $spec_dict cmd_info -summary {}]\n" set form_info [dict get $spec_dict form_info] dict for {fid finfo} $form_info { set form_synopsis [Dict_getdef $finfo -synopsis ""] if {$form_synopsis eq ""} { #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { + set form_synopsis [punk::args::synopsis -noheader -form $fid [dict get $spec_dict id]] + set ansifree_synopsis [punk::ansi::ansistripraw $form_synopsis] + if {[string length $ansifree_synopsis] > 90} { # - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] + set form_synopsis [punk::args::synopsis -noheader -return summary -form $fid [dict get $spec_dict id]] } + #review if {[string match (autodef)* $form_synopsis]} { set form_synopsis [string range $form_synopsis 9 end] } @@ -3428,17 +3593,18 @@ tcl::namespace::eval punk::args { set opt_names [list] set opt_names_display [list] + set opt_names_hints [list] ;#comments in first column below name display. set lookup_optset [dict create] if {[llength [dict get $form_dict OPT_NAMES]]} { set all_opts [list] - foreach optset [dict get $form_dict OPT_NAMES] { + foreach optionset [dict get $form_dict OPT_NAMES] { #e.g1 "-alias1|-realname" #e.g2 "-f|--filename" (fossil longopt style) #e.g3 "-f|--filename=" (gnu longopt style) - set optmembers [split $optset |] + set optmembers [split $optionset |] lappend all_opts {*}$optmembers foreach o $optmembers { - dict set lookup_optset $o $optset + dict set lookup_optset $o $optionset #goodargs } } @@ -3464,6 +3630,11 @@ tcl::namespace::eval punk::args { $trie destroy foreach optset [dict get $form_dict OPT_NAMES] { set arginfo [dict get $form_dict ARG_INFO $optset] + set parsekey [dict get $arginfo -parsekey] + set storageinfo "" + if {$parsekey ne "" && $parsekey ne $optset} { + set storageinfo "(stored as: $parsekey)" + } if {[dict get $arginfo -prefix]} { set opt_members [split $optset |] set odisplay [list] @@ -3475,8 +3646,7 @@ tcl::namespace::eval punk::args { set tail "" } else { set idlen [string length $id] - set prefix [string range $opt 0 $idlen-1] - set tail [string range $opt $idlen end] + lassign [punk::lib::string_splitbefore $opt $idlen] prefix tail } lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail } @@ -3485,12 +3655,23 @@ tcl::namespace::eval punk::args { } else { lappend opt_names_display $optset } + lappend opt_names_hints $storageinfo #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] lappend opt_names $optset } } else { set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names + foreach optset [dict get $form_dict OPT_NAMES] { + set arginfo [dict get $form_dict ARG_INFO $optset] + set parsekey [dict get $arginfo -parsekey] + set storageinfo "" + if {$parsekey ne "" && $parsekey ne $optset} { + set storageinfo "(stored as: $parsekey)" + } + lappend opt_names_display $optset + lappend opt_names_hints $storageinfo + } + #set opt_names_display $opt_names } } set leading_val_names [dict get $form_dict LEADER_NAMES] @@ -3509,18 +3690,84 @@ tcl::namespace::eval punk::args { # set leading_val_names {} #} set leading_val_names_display $leading_val_names + 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 $parsed_leaders] [list opts $opt_names_display $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { + 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 + set lastgroup "" + set lastgroup_parsekey "" + foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + + if {$argumentclass eq "opts"} { + set thisgroup [dict get $arginfo -group] + if {$thisgroup ne $lastgroup} { + if {[dict exists $form_dict OPT_GROUPS $thisgroup -parsekey]} { + set thisgroup_parsekey [dict get $form_dict OPT_GROUPS $thisgroup -parsekey] + } else { + set thisgroup_parsekey "" + } + + #footer/line? + if {$use_table} { + $t add_row [list " " "" "" "" ""] + } else { + lappend errlines " " + } + + if {$thisgroup eq ""} { + } else { + #SHOW group 'header' (not really a table header - just another row) + set help "" + if {[dict exists $form_dict OPT_GROUPS $thisgroup -help]} { + set help [dict get $form_dict OPT_GROUPS $thisgroup -help] + } + if {$thisgroup_parsekey eq ""} { + set groupinfo "(documentation group)" + } else { + set groupinfo "(common flag group)\nkey:$thisgroup_parsekey" + } + if {$use_table} { + $t add_row [list " $thisgroup" $groupinfo "" "" $help] + if {$arg eq $badarg} { + $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG + } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG + } + } else { + #review - formatting will be all over the shop due to newlines in typesshow, help + #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" + set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] + append arghelp \n + if {$arg eq $badarg} { + set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] + } elseif {$arg in $goodargs} { + set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] + } + foreach ln [split $help \n] { + append arghelp " $ln" \n + } + lappend errlines $arghelp + } + } + set lastgroup $thisgroup + set lastgroup_parsekey $thisgroup_parsekey + } + if {[dict exists $arginfo -parsekey]} { + set mypkey [dict get $arginfo -parsekey] + if {$mypkey eq "$lastgroup_parsekey" || $mypkey eq [string trimright [lindex [split $arg |] end] =]} { + set hint "" + } + } + } + if {[dict exists $arginfo -default]} { set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" } else { @@ -3656,14 +3903,15 @@ tcl::namespace::eval punk::args { } else { set shortestid [dict get $idents $c] } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } + lassign [punk::lib::string_splitbefore $c [string length $shortestid]] prefix tail + #if {$shortestid eq $c} { + # set prefix $c + # set tail "" + #} else { + # set idlen [string length $shortestid] + # set prefix [string range $c 0 $idlen-1] + # set tail [string range $c $idlen end] + #} set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] if {[llength $markers]} { set mk " [join $markers {}]" @@ -3849,7 +4097,12 @@ tcl::namespace::eval punk::args { } if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] + if {$hint ne ""} { + set col1 $argshow\n$hint + } else { + set col1 $argshow + } + $t add_row [list $col1 $typeshow $default $multiple $help] if {$arg eq $badarg} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG } elseif {$arg in $goodargs} { @@ -3857,7 +4110,13 @@ tcl::namespace::eval punk::args { } } else { #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" + set linetail " TYPE:$typeshow DEFAULT:$default MULTI:$multiple" + if {$hint ne ""} { + set arghelp [textblock::join -- "[a+ bold]$argshow\n$hint$RST" $linetail] + } else { + set arghelp "[a+ bold]$argshow$RST $linetail" + } + append arghelp \n if {$arg eq $badarg} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] } elseif {$arg in $goodargs} { @@ -4171,7 +4430,10 @@ tcl::namespace::eval punk::args { }]} { #unhappy path - not enough options #review - which form of punk::args::parse? - punk::args::parse $args withid ::punk::args::parse + #we expect this to always raise error - review + set result [punk::args::parse $args withid ::punk::args::parse] + puts stderr "punk::args::parse unexpected result $result" + return ;#failsafe } incr i -1 #lappend opts $a [lindex $opts_and_vals $i] @@ -4409,27 +4671,61 @@ tcl::namespace::eval punk::args { #set v [lindex $values end-$ridx] set v [lindex $all_remaining end] set tp [lindex $typelist 0] + # ----------------- + set tp [string trim $tp ?] ;#shouldn't be necessary #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole name should be marked -optional true instead. - set tp [string trim $tp ?] - foreach tp_member [split $tp |] { - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #plain "literal" without bracketed specifier - match to argument name - set match $clausename - } - if {$v eq $match} { - set alloc_ok 1 - lpop all_remaining - if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + #we shouldn't have an optional clause member if there is only one member - the whole argument should be marked -optional true instead. + # ----------------- + + #todo - support complex type members such as -type {{literal a|b} int OR} + #for now - require llength 1 - simple type such as -type {literal(ab)|int} + if {[llength $tp] !=1} { + error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + } + + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + switch -exact -- [lindex $tp_alternative 0] { + literal { + set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) + set match [lindex $tp_alternative 1] + if {$v eq $match} { + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + #the type (or one of the possible type alternates) matched a literal + break } - #type (or one of the possible type alternates) matched a literal - break } + stringstartswith { + set pfx [lindex $tp_alternative 1] + if {[string match "$pfx*" $v} { + set alloc_ok 1 + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + break + } + + } + stringendswith { + set sfx [lindex $tp_alternative 1] + if {[string match "*$sfx" $v} { + set alloc_ok 1 + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + break + } + + } + default {} } } if {!$alloc_ok} { @@ -4439,6 +4735,9 @@ tcl::namespace::eval punk::args { } } else { + #todo - use _split_type_expression + + #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) #This is better caught during definition. #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} @@ -4448,14 +4747,11 @@ tcl::namespace::eval punk::args { set alloc_count 0 #clause name may have more entries than types - extras at beginning are ignored set rtypelist [lreverse $typelist] - set rclausename [lrange [lreverse $clausename] 0 [llength $typelist]-1] - #assert length of rtypelist >= $rclausename set alloc_ok 0 set reverse_type_index 0 #todo handle type-alternates # for example: -type {string literal(x)|literal(y)} - foreach tp $rtypelist membername $rclausename { - #(membername may be empty if not enough elements) + foreach tp $rtypelist { #set rv [lindex $rcvals end-$alloc_count] set rv [lindex $all_remaining end-$alloc_count] if {[string match {\?*\?} $tp]} { @@ -4464,54 +4760,63 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { + switch -glob $tp { + literal* { + set litinfo [string range $tp 7 end] set match [string range $litinfo 1 end-1] - } else { - #if membername empty - equivalent to "literal()" - matches empty string literal - #edgecase - possibly? no need for empty-string literals - but allow it without error. - set match $membername - } - #todo -literalprefix - if {$rv eq $match} { - set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok - incr alloc_count - } else { - if {$clause_member_optional} { - # + #todo -literalprefix + if {$rv eq $match} { + set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok + incr alloc_count } else { - set alloc_ok 0 - break + if {$clause_member_optional} { + # + } else { + set alloc_ok 0 + break + } } } - } else { - if {$clause_member_optional} { - #review - optional non-literal makes things harder.. - #we don't want to do full type checking here - but we now risk allocating an item that should actually - #be allocated to the previous value - set prev_type [lindex $rtypelist $reverse_type_index+1] - if {[string match literal* $prev_type]} { - set litinfo [string range $prev_type 7 end] - #todo -literalprefix - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #prev membername - set match [lindex $rclausename $reverse_type_index+1] + "stringstartswith(*" { + set pfx [string range $tp 17 end-1] + if {[string match "$pfx*" $tp]} { + set alloc_ok 1 + incr alloc_count + } else { + if {!$clause_member_optional} { + set alloc_ok 0 + break } - if {$rv ne $match} { - #current val doesn't match previous type - allocate here + } + } + default { + if {$clause_member_optional} { + #review - optional non-literal makes things harder.. + #we don't want to do full type checking here - but we now risk allocating an item that should actually + #be allocated to the previous value + # todo - lsearch to next literal or non-optional? + set prev_type [lindex $rtypelist $reverse_type_index+1] + if {[string match literal* $prev_type]} { + set litinfo [string range $prev_type 7 end] + #todo -literalprefix + if {[string match (*) $litinfo]} { + set match [string range $litinfo 1 end-1] + } else { + set match [lindex $rclausename $reverse_type_index+1] + } + if {$rv ne $match} { + #current val doesn't match previous type - allocate here + incr alloc_count + } + } else { + #no literal to anchor against.. incr alloc_count } } else { - #no literal to anchor against.. + #allocate regardless of type - we're only matching on arity and literal positioning here. + #leave final type-checking for later. incr alloc_count } - } else { - #allocate regardless of type - we're only matching on arity and literal positioning here. - #leave final type-checking for later. - incr alloc_count } } incr reverse_type_index @@ -4522,7 +4827,8 @@ tcl::namespace::eval punk::args { set all_remaining [lrange $all_remaining 0 end-$alloc_count] #don't lpop if -multiple true if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + #lpop tailnames + ledit tailnames end end } } else { break @@ -4550,11 +4856,11 @@ tcl::namespace::eval punk::args { set alloc_count 0 set resultlist [list] set n [expr {[llength $thistype]-1}] - #name can have more or less items than typelist - set thisnametail [lrange $thisname end-$n end] set tpidx 0 set newtypelist $thistype - foreach tp $thistype membername $thisnametail { + set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] + foreach tp $thistype { + #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) set v [lindex $all_remaining $alloc_count] if {[string match {\?*\?} $tp]} { set clause_member_optional 1 @@ -4564,78 +4870,130 @@ tcl::namespace::eval punk::args { set tp [string trim $tp ?] set member_satisfied 0 + if {$has_choices} { + #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 + set member_satisfied 1 + } - #----------------------------------------------------------------------------------- - #first build list of any literals - and whether any are literalprefix - set literals [list] - set literalprefixes [list] - set nonliterals [list] - set dict_member_match [dict create] - foreach tp_member [split $tp |] { - #JJJJ - if {[string match literal* $tp_member]} { - if {[string match literalprefix* $tp_member]} { - set litinfo [string range $tp_member 13 end] - if {[string match (*) $litinfo]} { - lappend literalprefixes [string range $litinfo 1 end-1] - } else { - lappend literalprefixes $membername + + if {!$member_satisfied} { + #----------------------------------------------------------------------------------- + #first build category lists of any literal,literalprefix,stringstartwith,other + # + set ctg_literals [list] + set ctg_literalprefixes [list] + set ctg_stringstartswith [list] + set ctg_stringendswith [list] + set ctg_other [list] + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + #JJJJ + lassign $tp_alternative t textra + switch -exact -- $t { + literal { + lappend ctg_literals $textra } - dict set dict_member_match $tp_member [lindex $literalprefixes end] - } else { - set litinfo [string range $tp_member 7 end] - if {[string match (*) $litinfo]} { - lappend literals [string range $litinfo 1 end-1] - } else { - lappend literals $membername + literalprefix { + lappend ctg_literalprefixes $textra + } + stringstartswith { + lappend ctg_stringstartswith $textra + } + stringendswith { + lappend ctg_stringendswith $textra + } + default { + lappend ctg_other $tp_alternative + } + } + } + #----------------------------------------------------------------------------------- + if {[llength $ctg_other] > 0} { + #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align + #we don't do full validation here -leave main validation for later (review) + foreach tp_alternative $ctg_other { + switch -exact -- $tp_alternative { + int { + if {[string is integer -strict $v]} { + set member_satisfied 1 + break + } + } + double { + if {[string is double -strict $v]} { + set member_satisfied 1 + break + } + } + bool { + if {[string is boolean -strict $v]} { + set member_satisfied 1 + break + } + } + number { + if {[string is integer -strict $v] || [string is double -strict $v]} { + set member_satisfied 1 + break + } + } + dict { + if {[punk::args::lib::string_is_dict $v]} { + set member_satisfied 1 + break + } + } + default { + #REVIEW!!! + #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. + #todo - catch/detect in caller + set member_satisfied 1 + break + } } - dict set dict_member_match $tp_member [lindex $literals end] } - } else { - lappend nonliterals $tp_member } } - #----------------------------------------------------------------------------------- - #asert - each tp_member is a key in dict_member_match - if {[llength $nonliterals] > 0} { - #presence of any ordinary type as one of the alternates - means we consider it a match - #we don't validate here -leave validation for later (review) - set member_satisfied 1 - } else { - if {$v in $literals} { + + if {!$member_satisfied && ([llength $ctg_literals] || [llength $ctg_literalprefixes])} { + if {$v in $ctg_literals} { set member_satisfied 1 + lset newtypelist $tpidx validated-$tp } else { - #literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed + #ctg_literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed #(exact match would have been caught in other branch of this if) - set full_v [tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $v] - if {$full_v ne "" && $full_v ni $literals} { - #matched prefix must be for one of the entries in literalprefixes - valid + #review - how does ctg_stringstartswith affect prefix calc for literals? + set full_v [tcl::prefix::match -error "" [list {*}$ctg_literals {*}$ctg_literalprefixes] $v] + if {$full_v ne "" && $full_v ni $ctg_literals} { + #matched prefix must be for one of the entries in ctg_literalprefixes - valid + set member_satisfied 1 + set v $full_v ;#map prefix given as arg to the full literalprefix value + lset newtypelist $tpidx validated-$tp + } + } + } + if {!$member_satisfied && [llength $ctg_stringstartswith]} { + foreach pfx $ctg_stringstartswith { + if {[string match "$pfx*" $v]} { + set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + #review. consider multi-word typespec with RPN? + # {*}$tp_alternative validated + break + } + } + } + if {!$member_satisfied && [llength $ctg_stringendswith]} { + foreach pfx $ctg_stringendswith { + if {[string match "*$pfx" $v]} { set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + break } } } - #foreach tp_member [split $tp |] { - # if {[string match literal* $tp_member]} { - # #todo - support literal prefix-matching - # #e.g see ::readFile filename ?text|binary? - must accept something like readfile xxx.txt b - # set litinfo [string range $tp_member 7 end] - # if {[string match (*) $litinfo]} { - # set match [string range $litinfo 1 end-1] - # } else { - # set match $membername - # } - # set match [dict get $dict_member_match $tp_member] - # if {$v eq $match} { - # set member_satisfied 1 - # break - # } - # } else { - # #we don't validate here -leave validation for later (review) - # set member_satisfied 1 - # break - # } - #} + if {$member_satisfied} { if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { @@ -4648,6 +5006,7 @@ tcl::namespace::eval punk::args { lappend resultlist "" } } else { + #may have satisfied one of the basic type tests above lappend resultlist $v incr alloc_count } @@ -4677,58 +5036,1760 @@ tcl::namespace::eval punk::args { #so that they are not subject to type validation #such elements shouldn't be subject to validation if {$alloc_ok} { - set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] + #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - set d [dict create consumed 0 resultlist {} typelist $thistype] + puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + set d [dict create consumed 0 resultlist {} typelist $thistype] } #puts ">>>> _get_dict_can_assign_value $d" return $d } - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" + #_split_type_expression + #only handles toplevel 'or' for type_expression e.g int|char + #we have no mechanism for & - (although it would be useful) + #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) + #or perhaps more performant, RPN to avoid bracket parsing + #if literal(..), literalprefix(..), stringstartswith(..) etc can have pipe symbols and brackets etc - we can't just use split + #if we require -type to always be treated as a list - and if an element is length 1 - require it to + #have properly balanced brackets that don't contain | ( ) etc we can simplify - REVIEW + + #consider: + #1 basic syntax - only OR supported - limits on what chars can be put in 'textn' elements. + #mode -type literalprefix(text1)|literalprefix(text2) -optional 1 + #2 expanded syntax - supports arbitrary chars in 'textn' elements - but still doesn't support more complex OR/AND logic + #mode -type {{literalprefix text1 | literalprefix text2}} + #3 RPN (reverse polish notation) - somewhat unintuitive, but allows arbitrary textn, and complex OR/AND logic without brackets. + #(forth like - stack based definition of types) + #mode -type {literalprefix text1 literalprefix text2 OR} + #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + + proc _split_type_expression {type_expression} { + if {[llength $type_expression] == 1} { + #simple expressions of length one must be splittable on | + #disallowed: things such as literal(|) or literal(x|etc)|int + #these would have to be expressed as {literal |} and {literal x|etc | int} + set or_type_parts [split $type_expression |] + set type_alternatives [list] + foreach t $or_type_parts { + if {[regexp {([^\(^\)]*)\((.*)\)$} $t _ name val]} { + lappend type_alternatives [list $name $val] + } else { + lappend type_alternatives $t + } + } + return $type_alternatives + } else { + error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + #todo + #RPN reverse polish notation + #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + #equivalent logic: ((stringstartswith(x)|stringstartswith(y))&stringendswith(z))|int + # {int ; stringstartswith x stringstartswith y OR } + + #experimental.. seems like a pointless syntax. + #may as well just use list of lists with |(or) as the intrinsic operator instead of parsing this + #e.g {stringstartswith x | literal | | int} + set type_alternatives [list] + set expect_separator 0 + for {set w 0} {$w < [llength $type_expression]} {incr w} { + set word [lindex $type_expression $w] + if {$expect_separator} { + if {$word eq "|"} { + #pipe could be last entry - not strictly correct, but can ignore + set expect_separator 0 + continue + } else { + error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + } + } + switch -exact -- $word { + literal - literalprefix - stringstartswith - stringendswith - stringcontains { + if {$w+1 > [llength $type_expression]} { + #premature end - no arg available for type which requires one + error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + } + lappend type_alternatives [list $word [lindex $type_expression $w+1]] + incr w ;#consume arg + set expect_separator 1 + } + default { + #simple types such as int,double,string + lappend type_alternatives $word + set expect_separator 1 + } } } + return $type_alternatives } + } + #old version + ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ### #set type $type_expression ;#todo - 'split' on | + ### set vlist $clausevalues_raw + ### set vlist_check $clausevalues_check + + ### set type_alternatives [_split_type_expression $type_expression] + ### #each type_alternative is a list of varying length depending on arguments supported by first word. + ### #TODO? + ### #single element types: int double string etc + ### #two element types literal literalprefix stringstartswith stringendswith + ### #TODO + ### set stype [lindex $type_alternatives 0] + ### #e.g int + ### #e.g {literal blah)etc} + ### set type [lindex $stype 0] + ### #switch on first word of each stype + ### # + + ### #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + ### switch -- $type { + ### any {} + ### literal { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {$e ne $testval} { + ### set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### stringstartwith { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {![string match $testval* $e]} { + ### set msg "$argclass '$argname' for %caller% requires stringstartswith value '$argname'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### list { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is list -strict $e_check]} { + ### set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### tcl::dict::for {checkopt checkval} $thisarg_checks { + ### switch -- $checkopt { + ### -minsize { + ### # -1 for disable is as good as zero + ### if {[llength $e_check] < $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### -maxsize { + ### if {$checkval ne "-1"} { + ### if {[llength $e_check] > $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### indexexpression { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[catch {lindex {} $e_check}]} { + ### set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### string - ansistring - globstring { + ### #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + ### #we possibly don't want to always have to regex on things that don't pass the other more basic checks + ### # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + ### # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + ### # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + ### # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + ### # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + ### # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + ### #todo? - way to validate both unstripped and stripped? + ### set pass_quick_list_e [list] + ### set pass_quick_list_e_check [list] + ### set remaining_e $vlist + ### set remaining_e_check $vlist_check + ### #review - order of -regexprepass and -regexprefail in original rawargs significant? + ### #for now -regexprepass always takes precedence + ### set regexprepass [tcl::dict::get $thisarg -regexprepass] + ### set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + ### if {$regexprepass ne ""} { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + ### lappend pass_quick_list_e $clauseval + ### lappend pass_quick_list_e_check $clauseval_check + ### } + ### } + ### set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] + ### set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] + ### } + ### if {$regexprefail ne ""} { + ### foreach clauseval $remaining_e clauseval_check $remaining_e_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #puts "----> checking $e vs regex $regexprefail" + ### if {[regexp $regexprefail $e]} { + ### if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + ### #review - %caller% ?? + ### set msg [tcl::dict::get $thisarg -regexprefailmsg] + ### } else { + ### set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + ### } + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### switch -- $type { + ### ansistring { + ### #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + ### #.. so we need to look at the original values in $vlist not $vlist_check + + ### #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + ### #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + ### package require punk::ansi + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![punk::ansi::ta::detect $e]} { + ### set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### globstring { + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![regexp {[*?\[\]]} $e]} { + ### set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + + ### if {[tcl::dict::size $thisarg_checks]} { + ### foreach clauseval $remaining_e_check { + ### set e_check [lindex $clauseval $clausecolumn] + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsize [dict get $thisarg_checks -minsize] + ### # -1 for disable is as good as zero + ### if {[tcl::string::length $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsize [dict get $thisarg_checks -maxsize] + ### if {$checkval ne "-1"} { + ### if {[tcl::string::length $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### number { + ### #review - consider effects of Nan and Inf + ### #NaN can be considered as 'technically' a number (or at least a special numeric value) + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign {} low high ;#set both empty + ### lassign $range low high + + ### if {"$low$high" ne ""} { + ### if {[::tcl::mathfunc::isnan $e]} { + ### set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$low eq ""} { + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### int { + ### #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is integer -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign $range low high + ### if {"$low$high" ne ""} { + ### if {$low eq ""} { + ### #lowside unspecified - check only high + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### #highside unspecified - check only low + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### #high and low specified + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### double { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is double -strict $e_check]} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -typeranges]} { + ### set ranges [dict get $thisarg_checks -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### #todo - small-value double comparisons with error-margin? review + ### #todo - empty string for low or high + ### lassign $range low high + ### if {$e_check < $low || $e_check > $high} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### bool { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is boolean -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### dict { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[llength $e_check] %2 != 0} { + ### set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsizes [dict get $thisarg_checks -minsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set minsize [lindex $minsizes $clausecolumn] + ### # -1 for disable is as good as zero + ### if {[tcl::dict::size $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsizes [dict get $thisarg_checks -maxsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set maxsize [lindex $maxsizes $clausecolumn] + ### if {$maxsize ne "-1"} { + ### if {[tcl::dict::size $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### alnum - + ### alpha - + ### ascii - + ### control - + ### digit - + ### graph - + ### lower - + ### print - + ### punct - + ### space - + ### upper - + ### wordchar - + ### xdigit { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is $type -strict $e_check]} { + ### set e [lindex $clauseval $t] + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### file - + ### directory - + ### existingfile - + ### existingdirectory { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #//review - we may need '?' char on windows + ### if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + ### #what about special file names e.g on windows NUL ? + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$type eq "existingfile"} { + ### if {![file exists $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } elseif {$type eq "existingdirectory"} { + ### if {![file isdirectory $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### char { + ### #review - char vs unicode codepoint vs grapheme? + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[tcl::string::length $e_check] != 1} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### default { + ### } + ### } + + ###} + + #new version + #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname + #common basic case: list of single item being a single value clause. + #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} + proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + #default_type_expression is for the chosen clausecolumn + #if {$argname eq "frametype"} { + #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" + #puts "--->list_of_clauses_raw : $list_of_clauses_raw" + #puts "--->list_of_clauses_check: $list_of_clauses_check" + #puts "--->$argname -type: [dict get $thisarg -type]" + #} - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value + set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn + + set default_type_alternatives [_split_type_expression $default_type_expression] + #--------------------- + #pre-calc prefix sets based on the default. + set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] + set literals [lmap v $alt_literals {lindex $v 1}] + set alt_literalprefixes [lsearch -all -inline -index 0 $default_type_alternatives literalprefix] + set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + #--------------------- + + #each type_alternative is a list of varying length depending on arguments supported by first word. + #TODO? + #single element types: int double string etc + #two element types literal literalprefix stringstartswith stringendswith + #TODO + + #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $default_type_alternatives] _]] + #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + #-types {int|char|literal(ok) char double} + #we are checking a and 1 against the defaulttype_expression e.g int|char|literal(ok) (type_alternatives = {int char literal(ok)} + #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + #review: for a particular clause the active type_expression might be overridden with 'any' if the column has already passed a -choices test + # + + set e_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_raw *] + set check_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] + set typelist_vals_raw [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_types *] + set typelist_vals [lmap v $typelist_vals_raw {string trim $v ?}] + + set c_idx -1 + foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { + incr c_idx + set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set firstany [lsearch -exact $col_type_alternatives any] + if {$firstany > -1} { + lset clause_results $c_idx $firstany 1 + continue + } + set a_idx -1 + foreach typealt $col_type_alternatives { + incr a_idx + lassign $typealt type testval ;#testval will be empty for basic types, but applies to literal, literalprefix, stringstartswith etc. + switch -exact -- $type { + literal { + if {$e ne $testval} { + set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } + } + literalprefix { + #this specific literalprefix testval value not relevant - we're testing against all in the set of typealternates + set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + if {$match ne "" && $match ni $literals} { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } else { + set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringstartswith { + if {[string match $testval* $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringendswith { + if {[string match *$testval $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + list { + if {![tcl::string::is list -strict $e_check]} { + set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + continue + } else { + if {[dict exists $thisarg_checks -minsize]} { + # -1 for disable is as good as zero + set minsize [dict get $thisarg_checks -minsize] + if {[llength $e_check] < $minsize} { + set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exist $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$maxsize ne "-1"} { + if {[llength $e_check] > $maxsize} { + set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + indexexpression { + if {[catch {lindex {} $e_check}]} { + set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + string - ansistring - globstring { + #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + #todo? - way to validate both unstripped and stripped? + #review - order of -regexprepass and -regexprefail in original rawargs significant? + #for now -regexprepass always takes precedence + #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + set regexprepass [tcl::dict::get $thisarg -regexprepass] + set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + if {$regexprepass ne ""} { + if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + lset clause_results $c_idx $a_idx 1 + break + } + } + if {$regexprefail ne ""} { + #puts "----> checking $e vs regex $regexprefail" + if {[regexp $regexprefail $e]} { + if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + #review - %caller% ?? + set msg [tcl::dict::get $thisarg -regexprefailmsg] + } else { + set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + } + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + #review - tests? + continue + } + } + switch -- $type { + ansistring { + #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + package require punk::ansi + if {![punk::ansi::ta::detect $e]} { + set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + globstring { + if {![regexp {[*?\[\]]} $e]} { + set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsize [dict get $thisarg_checks -minsize] + # -1 for disable is as good as zero + if {[tcl::string::length $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$checkval ne "-1"} { + if {[tcl::string::length $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + number { + #review - consider effects of Nan and Inf + #NaN can be considered as 'technically' a number (or at least a special numeric value) + if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign {} low high ;#set both empty + lassign $range low high + if {"$low$high" ne ""} { + if {[::tcl::mathfunc::isnan $e]} { + set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {$low eq ""} { + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + int { + #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + if {![tcl::string::is integer -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign $range low high + if {"$low$high" ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + #high and low specified + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + double { + if {![tcl::string::is double -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg_checks -typeranges]} { + set ranges [dict get $thisarg_checks -typeranges] + set range [lindex $ranges $clausecolumn] + #todo - small-value double comparisons with error-margin? review + lassign $range low high + if {$low$high ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass $argname for %caller% must be double less than or equal to $high. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass $argname for %caller% must be double greater than or equal to $low. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$e_check < $low || $e_check > $high} { + set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + bool { + if {![tcl::string::is boolean -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + dict { + if {[llength $e_check] %2 != 0} { + set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsizes [dict get $thisarg_checks -minsize] + set minsize [lindex $minsizes $clausecolumn] + # -1 for disable is as good as zero + if {[tcl::dict::size $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type minsize $minsize] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [lindex $maxsizes $clausecolumn] + if {$maxsize ne "-1"} { + if {[tcl::dict::size $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type maxsize $maxsize] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + } + alnum - + alpha - + ascii - + control - + digit - + graph - + lower - + print - + punct - + space - + upper - + wordchar - + xdigit { + if {![tcl::string::is $type -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + file - + directory - + existingfile - + existingdirectory { + #//review - we may need '?' char on windows + if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + #what about special file names e.g on windows NUL ? + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {$type eq "existingfile"} { + if {![file exists $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } elseif {$type eq "existingdirectory"} { + if {![file isdirectory $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + lset clause_results $c_idx $a_idx 1 + } + char { + #review - char vs unicode codepoint vs grapheme? + if {[tcl::string::length $e_check] != 1} { + set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + tk_screen_units { + switch -exact -- [string index $e_check end] { + c - i - m - p { + set numpart [string range $e_check 0 end-1] + if {![tcl::string::is double $numpart]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + default { + if {![tcl::string::is double $e_check]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + default { + #default pass for unrecognised types - review. + lset clause_results $c_idx $a_idx 1 + break + } + } + } + } + + foreach clauseresult $clause_results { + if {[lsearch $clauseresult 1] == -1} { + #no pass for this clause - fetch first? error and raise + #todo - return error containing clause_indices so we can report more than one failing element at once? + foreach e $clauseresult { + switch -exact [lindex $e 0] { + errorcode { + #errorcode msg checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against type_expression $type_expression" + # puts "--->list_of_clauses_raw : $list_of_clauses_raw" + # puts "--->list_of_clauses_check: $list_of_clauses_check" + # puts "--->$argname -type: [dict get $thisarg -type]" + # } + + # set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just passed type_expression + + # #set vlist [list] + # set clauses_dict [dict create] ;#key is ordinal position, remove entries as they are satsified + # set cidx -1 + # foreach cv $list_of_clauses_raw { + # incr cidx + # #REVIEW + # #if {$clause_size ==1} { + # # lappend vlist [list $cidx [list $cv]] + # #} else { + # #lappend vlist [list $cidx $cv] ;#store the index so we can reduce vlist as we go + # dict set clauses_dict $cidx $cv + # #} + # } + # #set vlist_check [list] + # set clauses_dict_check [dict create] + # set cidx -1 + # foreach cv $list_of_clauses_check { + # incr cidx + # #if {$clause_size == 1} { + # # lappend vlist_check [list $cidx [list $cv]] + # #} else { + # #lappend vlist_check [list $cidx $cv] + # dict set clauses_dict_check $cidx $cv + # #} + # } + + # set type_alternatives [_split_type_expression $type_expression] + # #each type_alternative is a list of varying length depending on arguments supported by first word. + # #TODO? + # #single element types: int double string etc + # #two element types literal literalprefix stringstartswith stringendswith + # #TODO + + # #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + # set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $type_alternatives] _]] + # #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + # #-types {int|char|literal(ok) char double} + # #we are checking a and 1 against the type_expression int|char|literal(ok) (type_alternatives = {int char literal(ok)} + # #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + # # + + + # set a_idx -1 + # foreach typealt $type_alternatives { + # incr a_idx + + # set type [lindex $typealt 0] + # #e.g int + # #e.g {literal blah} + # #e.g {literalprefix abc} + + # #switch on first word of each typealt + # # + + # #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + # switch -- $type { + # any {} + # literal { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {$e ne $testval} { + # set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # literalprefix { + # set alt_literals [lsearch -all -inline -index 0 $type_alternatives literal] + # set literals [lmap v $alt_literals {lindex $v 1}] + # set alt_literalprefixes [lsearch -all -inline -index 0 $type_alternatives literalprefix] + # set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # #this specific literalprefix value not relevant - we're testing against all in the set of typealternates + # #set testval [lindex $typealt 1] + # set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + # if {$match ne "" && $match ni $literals} { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringstartswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match $testval* $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringendswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match *$testval $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # list { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # if {![tcl::string::is list -strict $e_check]} { + # set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } else { + # if {[dict exists $thisarg_checks -minsize]} { + # # -1 for disable is as good as zero + # set minsize [dict get $thisarg_checks -minsize] + # if {[llength $e_check] < $minsize} { + # set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exist $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$maxsize ne "-1"} { + # if {[llength $e_check] > $maxsize} { + # set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # indexexpression { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[catch {lindex {} $e_check}]} { + # set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # string - ansistring - globstring { + # #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + # #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + # #todo? - way to validate both unstripped and stripped? + # #review - order of -regexprepass and -regexprefail in original rawargs significant? + # #for now -regexprepass always takes precedence + # #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + # set regexprepass [tcl::dict::get $thisarg -regexprepass] + # set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + # if {$regexprepass ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # if {$regexprefail ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # #puts "----> checking $e vs regex $regexprefail" + # if {[regexp $regexprefail $e]} { + # if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + # #review - %caller% ?? + # set msg [tcl::dict::get $thisarg -regexprefailmsg] + # } else { + # set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + # } + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + # #review - tests? + # } + # } + # } + # switch -- $type { + # ansistring { + # #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + # #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + # #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + # #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + # package require punk::ansi + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![punk::ansi::ta::detect $e]} { + # set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # globstring { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![regexp {[*?\[\]]} $e]} { + # set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # } + + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # if {[tcl::dict::size $thisarg_checks]} { + # set passed_checks 1 + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[dict exists $thisarg_checks -minsize]} { + # set minsize [dict get $thisarg_checks -minsize] + # # -1 for disable is as good as zero + # if {[tcl::string::length $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$checkval ne "-1"} { + # if {[tcl::string::length $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } else { + # if {[lindex $clause_results $c_idx $a_idx] eq "_"} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # number { + # #review - consider effects of Nan and Inf + # #NaN can be considered as 'technically' a number (or at least a special numeric value) + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign {} low high ;#set both empty + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {[::tcl::mathfunc::isnan $e]} { + # set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$low eq ""} { + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + + # } + # int { + # #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is integer -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {$low eq ""} { + # #lowside unspecified - check only high + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # #highside unspecified - check only low + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # #high and low specified + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # double { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is double -strict $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg_checks -typeranges]} { + # set ranges [dict get $thisarg_checks -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # #todo - small-value double comparisons with error-margin? review + # #todo - empty string for low or high + # set passed_checks 1 + # lassign $range low high + # if {$low$high ne ""} { + # if {$e_check < $low || $e_check > $high} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # bool { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is boolean -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # dict { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # puts "check_clausecolumn2 dict handler: c_idx:$c_idx clausecolumn:$clausecolumn clauseval_check:$clauseval_check" + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[llength $e_check] %2 != 0} { + # set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # } + # } + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set passed_checks 1 + # if {[tcl::dict::size $thisarg_checks]} { + # if {[dict exists $thisarg_checks -minsize]} { + # set minsizes [dict get $thisarg_checks -minsize] + # set e_check [lindex $clauseval_check $clausecolumn] + # set minsize [lindex $minsizes $clausecolumn] + # # -1 for disable is as good as zero + # if {[tcl::dict::size $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set e_check [lindex $clauseval_check $clausecolumn] + # set maxsize [lindex $maxsizes $clausecolumn] + # if {$maxsize ne "-1"} { + # if {[tcl::dict::size $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # alnum - + # alpha - + # ascii - + # control - + # digit - + # graph - + # lower - + # print - + # punct - + # space - + # upper - + # wordchar - + # xdigit { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is $type -strict $e_check]} { + # set e [lindex $clauseval $t] + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # file - + # directory - + # existingfile - + # existingdirectory { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + + # #//review - we may need '?' char on windows + # set passed_checks 1 + # if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + # #what about special file names e.g on windows NUL ? + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$type eq "existingfile"} { + # if {![file exists $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$type eq "existingdirectory"} { + # if {![file isdirectory $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # char { + # #review - char vs unicode codepoint vs grapheme? + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[tcl::string::length $e_check] != 1} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # tk_screen_units { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # switch -exact -- [string index $e_check end] { + # c - i - m - p { + # set numpart [string range $e_check 0 end-1] + # if {![tcl::string::is double $numpart]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # default { + # if {![tcl::string::is double $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # default { + # #default pass for unrecognised types - review. + # dict for {c_idx clauseval} $clauses_dict { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # foreach clauseresult $clause_results { + # if {[lsearch $clauseresult 1] == -1} { + # #no pass for this clause - fetch first? error and raise + # #todo - return error containing clause_indices so we can report more than one failing element at once? + # foreach e $clauseresult { + # if {[lindex $e 0] eq "errorcode"} { + # #errorcode msg values + #[para]ARGUMENTS: + #[list_begin arguments] + #[arg_def list-of-multiline-string deflist] + #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced + #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values + #[para]Each optionspec line defining a flag must be of the form: + #[para]-optionname -key val -key2 val2... + #[para]where the valid keys for each option specification are: -default -type -range -choices -optional etc + #[para]Each optionspec line defining a positional argument is of the form: + #[para]argumentname -key val -ky2 val2... + #[para]where the valid keys for each option specification are: -default -type -range -choices + #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. #[arg_def list rawargs] @@ -4763,7 +6824,7 @@ tcl::namespace::eval punk::args { tcl::dict::with argspecs {} ;#turn keys into vars #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names # ----------------------------------------------- - set opt_form [dict get $opts -form] + set opt_form [dict get $proc_opts -form] if {$opt_form eq "*"} { set selected_forms $form_names } elseif {[string is integer -strict $opt_form]} { @@ -4802,20 +6863,18 @@ tcl::namespace::eval punk::args { if {$VAL_MIN eq ""} { set valmin 0 #set VAL_MIN 0 - foreach v $VAL_NAMES { - if {![dict get $ARG_INFO $v -optional]} { - # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) - # e.g -types {a ?xxx?} - #this has one required and one optional - set typelist [dict get $ARG_INFO $v -type] - set clause_length 0 - foreach t $typelist { - if {![string match {\?*\?} $t]} { - incr clause_length - } + foreach v $VAL_REQUIRED { + # todo variable clause lengths (items marked optional in types using leading&trailing questionmarks) + # e.g -types {a ?xxx?} + #this has one required and one optional + set typelist [dict get $ARG_INFO $v -type] + set clause_length 0 + foreach t $typelist { + if {![string match {\?*\?} $t]} { + incr clause_length } - incr valmin $clause_length } + incr valmin $clause_length } } else { set valmin $VAL_MIN @@ -4826,19 +6885,18 @@ tcl::namespace::eval punk::args { set argnames [tcl::dict::keys $ARG_INFO] #set optnames [lsearch -all -inline $argnames -*] #JJJ - set all_opts [list] set lookup_optset [dict create] foreach optset $OPT_NAMES { #optset e.g {-x|--longopt|--longopt=|--otherlongopt} - set optmembers [split $optset |] - foreach optdef $optmembers { + foreach optdef [split $optset |] { set opt [string trimright $optdef =] - if {$opt ni $all_opts} { + if {![dict exists $lookup_optset $opt]} { dict set lookup_optset $opt $optset - lappend all_opts $opt } } } + set all_opts [dict keys $lookup_optset] + set ridx 0 set rawargs_copy $rawargs set remaining_rawargs $rawargs @@ -4863,206 +6921,277 @@ tcl::namespace::eval punk::args { #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" #} + set can_have_leaders 1 ;#default assumption + if {$LEADER_MAX == 0 || ([llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED)} { + set can_have_leaders 0 + } #REVIEW - this attempt to classify leaders vs opts vs values doesn't account for leaders with clauses containing optional elements - #e.g @leadrs {x -type {int ?int?}} + #e.g @leaders {x -type {int ?int?}} set nameidx 0 - if {$LEADER_MAX != 0} { - for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { - set raw [lindex $rawargs $ridx] ;#received raw arg - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $nameidx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > $named_leader_args_max-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string - } - if {$OPT_MAX ne "0"} { - #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately - set flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader + if {$can_have_leaders} { + if {$LEADER_TAKEWHENARGSMODULO} { + #assign set of leaders purely based on number of total args + set take [expr {[llength $remaining_rawargs] % $LEADER_TAKEWHENARGSMODULO}] + set pre_values [lrange $remaining_rawargs 0 $take-1] + set remaining_rawargs [lrange $remaining_rawargs $take end] + } else { + #greedy taking of leaders based on type-matching + + set leadernames_seen [list] + for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { + set raw [lindex $rawargs $ridx] ;#received raw arg + if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { break } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - set leader_type [dict get $ARG_INFO $leader_posn_name -type] - #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" - set clauselength [llength $leader_type] - set min_clauselength 0 - foreach t $leader_type { - if {![string match {\?*\?} $t]} { - incr min_clauselength + if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { + #at last named leader + set leader_posn_name [lindex $LEADER_NAMES $nameidx] + if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { + set is_multiple 1 } - } - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] $raw] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop remaining_rawargs 0] - # incr ridx - # continue - # } - #} - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break + } elseif {$ridx > $named_leader_args_max-1} { + #beyond names - retain name if -multiple was true + if {!$is_multiple} { + set leader_posn_name "" } - - #check if enough remaining_rawargs to fill any required values - if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + } else { + set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string + } + if {$OPT_MAX ne "0" && [string match -* $raw]} { + #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately + set possible_flagname $raw + if {[string match --* $raw]} { + set eposn [string first = $raw] + # --flag=xxx + if {$eposn >=3} { + set possible_flagname [string range $raw 0 $eposn-1] + } + } + set matchopt [::tcl::prefix::match -error {} $all_opts $possible_flagname] + if {$matchopt ne ""} { + #flaglike matches a known flag - don't treat as leader break } + } - #leadername may be a 'clause' of arbitrary length (e.g -type {int double} or {int string number}) - set end_leaders 0 + #for each branch - break or lappend + if {$leader_posn_name ne ""} { + set leader_type [dict get $ARG_INFO $leader_posn_name -type] + #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" + set clauselength [llength $leader_type] + set min_clauselength 0 foreach t $leader_type { - set raw [lindex $rawargs $ridx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - set flagname $raw - if {[string match --* $raw]} { - set eposn [string first = $raw] - # --flag=xxx - if {$eposn >=3} { - set flagname [string range $raw 0 $eposn-1] - } - } - set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - lappend pre_values [lpop remaining_rawargs 0] - incr ridx - } - } else { - lappend pre_values [lpop remaining_rawargs 0] - incr ridx + if {![string match {\?*\?} $t]} { + incr min_clauselength } } - incr ridx -1 ;#leave ridx at index of last r that we set - if {$end_leaders} { - break - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } else { - #clause is required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one complete clause for this name - requirement satisfied - now equivalent to optional + if {$leader_posn_name ni $LEADER_REQUIRED} { + #optional leader + + #most adhoc arg processing will allocate based on number of args rather than matching choice values first + #(because a choice value could be a legitimate data value) + + #review - option to process in this manner? + #first check if the optional leader value is a match for a choice ? + #if {[dict exists $arg_info $leader_posn_name -choices]} { + # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] $raw] + # if {$vmatch ne ""} { + # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values + # lappend pre_values [lpop remaining_rawargs 0] + # incr ridx + # continue + # } + #} if {[llength $remaining_rawargs] < $min_clauselength} { #not enough remaining args to fill even this optional leader #rather than raise error here - perform our break (for end of leaders) and let the code below handle it break } + #check if enough remaining_rawargs to fill any required values if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { break } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values - #we still need to check if enough values for the leader itself - if {[llength $remaining_rawargs] < $min_clauselength} { - #not enough remaining args to fill *required* leader - break - } - set end_leaders 0 - foreach t $leader_type { - set raw [lindex $rawargs $ridx] - if {[string match {\?*\?} $t] && [string match -* $raw]} { - #review - limitation of optional leaders is they can't be same value as any defined flags/opts - - set matchopt [::tcl::prefix::match -error {} $all_opts $raw] - if {$matchopt ne ""} { - #don't consume if flaglike (and actually matches an opt) - set end_leaders 1 - break ;#break out of looking at -type members in the clause - } else { - #unrecognised flag - treat as value for optional member of the clause - lappend pre_values [lpop remaining_rawargs 0] - incr ridx + #leadername may be a 'clause' of arbitrary length (e.g -type {int double} or {int string number}) + set end_leaders 0 + set tentative_pre_values [list] + set tentative_idx $ridx + if {$OPT_MAX ne "0"} { + foreach t $leader_type { + set raw [lindex $rawargs $tentative_idx] + if {[string match {\?*\?} $t] && [string match -* $raw]} { + #review - limitation of optional leaders is they can't be same value as any defined flags/opts + set flagname $raw + if {[string match --* $raw]} { + set eposn [string first = $raw] + # --flag=xxx + if {$eposn >=3} { + set flagname [string range $raw 0 $eposn-1] + } + } + set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] + if {$matchopt ne ""} { + #don't consume if flaglike (and actually matches an opt) + set end_leaders 1 + break ;#break out of looking at -type members in the clause + } else { + #unrecognised flag - treat as value for optional member of the clause + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } + } else { + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } + } + if {$end_leaders} { + break } } else { - lappend pre_values [lpop remaining_rawargs 0] - incr ridx + foreach t $leader_type { + #JJJ + set raw [lindex $rawargs $tentative_idx] + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } } - } - incr ridx -1 - if {$end_leaders} { - break - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break + set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set consumed [dict get $assign_d consumed] + set resultlist [dict get $assign_d resultlist] + set newtypelist [dict get $assign_d typelist] + if {$consumed != 0} { + if {$leader_posn_name ni $leadernames_seen} { + lappend leadernames_seen $leader_posn_name + } + dict incr leader_posn_names_assigned $leader_posn_name + #for {set c 0} {$c < $consumed} {incr c} { + # lappend pre_values [lpop remaining_rawargs 0] + #} + lappend pre_values {*}[lrange $remaining_rawargs 0 $consumed-1] + ledit remaining_rawargs 0 $consumed-1 + + incr ridx $consumed + incr ridx -1 ;#leave ridx at index of last r that we set } else { - if {$valmin > 0} { - if {[llength $remaining_rawargs] > $valmin} { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name + + } + if {!$is_multiple} { + incr nameidx + } + } else { + #clause is required + if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { + #already accepted at least one complete clause for this name - requirement satisfied - now equivalent to optional + if {[llength $remaining_rawargs] < $min_clauselength} { + #not enough remaining args to fill even this optional leader + #rather than raise error here - perform our break (for end of leaders) and let the code below handle it + break + } + + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + break + } + } + #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values + #we still need to check if enough values for the leader itself + if {[llength $remaining_rawargs] < $min_clauselength} { + #not enough remaining args to fill *required* leader + break + } + + set end_leaders 0 + + #review - are we allowing multivalue leader clauses where the optional members are not at the tail? + #eg @leaders {double -type {?int? char}} + #as we don't type-check here while determining leaders vs opts vs values - this seems impractical. + #for consistency and simplification - we should only allow optional clause members at the tail + # and only for the last defined leader. This should be done in the definition parsing - not here. + foreach t $leader_type { + set raw [lindex $rawargs $ridx] + if {[string match {\?*\?} $t] && [string match -* $raw]} { + #review - limitation of optional leaders is they can't be same value as any defined flags/opts + + set matchopt [::tcl::prefix::match -error {} $all_opts $raw] + if {$matchopt ne ""} { + #don't consume if flaglike (and actually matches an opt) + set end_leaders 1 + break ;#break out of looking at -type members in the clause } else { - break + #unrecognised flag - treat as value for optional member of the clause + #ridx must be valid if we matched -* - so lpop will succeed + lappend pre_values [lpop remaining_rawargs 0] + incr ridx } } else { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name + if {[string match {\?*\?} $t]} { + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + set end_leaders 1 + break + } + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break + } + } else { + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break ;#let validation of required leaders report the error? + } + } + incr ridx } } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop remaining_rawargs 0] + incr ridx -1 + if {$end_leaders} { + break + } + if {!$is_multiple} { + incr nameidx + } dict incr leader_posn_names_assigned $leader_posn_name } } else { - #review - if is_multiple, keep going if enough remaining_rawargs for values? - break + #unnamed leader + if {$LEADER_MIN ne "" } { + if {$ridx > $LEADER_MIN-1} { + if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { + break + } else { + if {$valmin > 0} { + if {[llength $remaining_rawargs] > $valmin} { + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } else { + break + } + } else { + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } + } + } else { + #haven't reached LEADER_MIN + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } + } else { + #review - if is_multiple, keep going if enough remaining_rawargs for values? + break + } } - } - #incr ridx - } ;# end foreach r $rawargs_copy + #incr ridx + } ;# end foreach r $rawargs_copy + } } #puts "get_dict ================> pre: $pre_values" @@ -5075,13 +7204,21 @@ tcl::namespace::eval punk::args { set leadermin $LEADER_MIN } if {$LEADER_MAX eq ""} { - set leadermax -1 + if {[llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED} { + set leadermax 0 + } else { + set leadermax -1 + } } else { set leadermax $LEADER_MAX } if {$VAL_MAX eq ""} { - set valmax -1 + if {[llength $VAL_NAMES] == 0 && !$VAL_UNNAMED} { + set valmax 0 + } else { + set valmax -1 + } } else { set valmax $VAL_MAX } @@ -5090,12 +7227,6 @@ tcl::namespace::eval punk::args { #assert - remaining_rawargs has been reduced by leading positionals set opts [dict create] ;#don't set to OPT_DEFAULTS here - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> pre_values: $pre_values" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} set leaders [list] set arglist {} @@ -5123,32 +7254,8 @@ tcl::namespace::eval punk::args { break } set a [lindex $remaining_rawargs $i] - #if {$a eq "--"} { - # #REVIEW - # #remaining num args <= valmin already covered above - # if {$valmax != -1} { - # #finite max number of vals - # if {$remaining_args_including_this == $valmax} { - # #assume it's a value. - # set arglist [lrange $remaining_rawargs 0 $i-1] - # set post_values [lrange $remaining_rawargs $i end] - # } else { - # #assume it's an end-of-options marker - # lappend flagsreceived -- - # set arglist [lrange $remaining_rawargs 0 $i] - # set post_values [lrange $remaining_rawargs $i+1 end] - # } - # } else { - # #unlimited number of post_values accepted - # #treat this as eopts - we don't care if remainder look like options or not - # lappend flagsreceived -- - # set arglist [lrange $remaining_rawargs 0 $i] - # set post_values [lrange $remaining_rawargs $i+1 end] - # } - # break - #} - if {[string match --* $a]} { - if {$a eq "--"} { + switch -glob -- $a { + -- { if {$a in $OPT_NAMES} { #treat this as eopts - we don't care if remainder look like options or not lappend flagsreceived -- @@ -5160,35 +7267,37 @@ tcl::namespace::eval punk::args { set post_values [lrange $remaining_rawargs $i end] } break - } else { + } + --* { set eposn [string first = $a] if {$eposn > 2} { #only allow longopt-style = for double leading dash longopts #--*==0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { @@ -5374,25 +7532,25 @@ tcl::namespace::eval punk::args { if {[string match --* $a] && $eposn > 2} { #only allow longopt-style = for double leading dash longopts #--*= --x) + lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x) } else { if {[llength $OPT_NAMES]} { set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" @@ -5446,8 +7610,8 @@ tcl::namespace::eval punk::args { } } else { #not a flag/option - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] break } } @@ -5471,26 +7635,114 @@ tcl::namespace::eval punk::args { #} #--------------------------------------- + #Order the received options by the order in which they are *defined* + #EXCEPT that grouped options using same parsekey must be processed in received order set ordered_opts [dict create] - set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] - #unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) - # e.g -fg|-foreground - # e.g -x|--fullname= - #Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} - foreach o $unaliased_opts optset $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $optset]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $optset] + + #set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] + ##unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) + ## e.g -fg|-foreground + ## e.g -x|--fullname= + ##Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} + #foreach o $unaliased_opts optset $OPT_NAMES { + # if {[dict exists $opts $o]} { + # dict set ordered_opts $o [dict get $opts $o] + # } elseif {[dict exists $OPT_DEFAULTS $optset]} { + # #JJJ + # set parsekey "" + # if {[tcl::dict::exists $argstate $o -parsekey]} { + # set parsekey [tcl::dict::get $argstate $o -parsekey] + # } + # if {$parsekey eq ""} { + # set parsekey $o + # } + # dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + # } + #} + + #puts ">>>>==== $opts" + set seen_pks [list] + #treating opts as list for this loop. + foreach optset $OPT_NAMES { + set parsekey "" + set has_parsekey_override 0 + if {[tcl::dict::exists $argstate $optset -parsekey]} { + set parsekey [tcl::dict::get $argstate $optset -parsekey] + } + if {$parsekey eq ""} { + set has_parsekey_override 0 + #fall back to last element of aliased option e.g -fg|-foreground -> "-foreground" + set parsekey [string trimright [lindex [split $optset |] end] =] + } else { + set has_parsekey_override 1 + } + lappend seen_pks $parsekey + set is_found 0 + set foundkey "" + set foundval "" + #no lsearch -stride avail in 8.6 + foreach {k v} $opts { + if {$k eq $parsekey} { + set foundkey $k + set is_found 1 + set foundval $v + #can be multiple - last match wins - don't 'break' out of foreach + } + } ;#avoiding further dict/list shimmering + #if {[dict exists $opts $parsekey]} { + # set found $parsekey + # set foundval [dict get $opts $parsekey] + #} + if {!$is_found && $parsekey ne $optset} { + #.g we may have in opts things like: -decreasing|-SORTDIRECTION -increasing|-SORTDIRECTION + #(where -SORTDIRECTION was configured as -parsekey) + #last entry must win + #NOTE - do not use dict for here. opts is not strictly a dict - dupe keys will cause wrong ordering + foreach {o v} $opts { + if {[string match *|$parsekey $o]} { + set foundkey $o + set is_found 1 + set foundval $v + #last match wins - don't 'break' out of foreach + } + } + } + if {$is_found} { + dict set ordered_opts $foundkey $foundval + } elseif {[tcl::dict::exists $OPT_DEFAULTS $optset]} { + if {$parsekey ne $optset} { + set tailopt [string trimright [lindex [split $optset |] end] =] + if {$tailopt ne $parsekey} { + #defaults for multiple options sharing a -parsekey value ? review + dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + } + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + } } } + #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval + #But make sure not to add any repeated parsekey e.g -increasing|-SORT -decreasing|-SORT + #use the seen_pks from the ordered_opts loop above + #keep working with opts only as list here.. + if {[llength $opts] > 2*[dict size $ordered_opts]} { + foreach {o o_val} $opts { + lassign [split $o |] _ parsekey ;#single pipe - 2 elements only | + if {$parsekey ne "" && $parsekey in $seen_pks} { + continue + } + if {![dict exists $ordered_opts $o]} { + dict set ordered_opts $o $o_val + } } } set opts $ordered_opts + #opts is a proper dict now + + #NOTE opts still may contain some entries in non-final form such as -flag|-PARSEKEY #--------------------------------------- @@ -5509,6 +7761,9 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + + set start_position $positionalidx set nameidx 0 #MAINTENANCE - (*nearly*?) same loop logic as for value @@ -5517,6 +7772,7 @@ tcl::namespace::eval punk::args { set ldr [lindex $leaders $ldridx] if {$leadername ne ""} { set leadertypelist [tcl::dict::get $argstate $leadername -type] + set leader_clause_size [llength $leadertypelist] set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] @@ -5524,7 +7780,8 @@ tcl::namespace::eval punk::args { set newtypelist [dict get $assign_d typelist] if {[tcl::dict::get $argstate $leadername -optional]} { if {$consumed == 0} { - #error 111 + puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -5538,7 +7795,8 @@ tcl::namespace::eval punk::args { set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg } else { - #error 222 + puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -5547,11 +7805,17 @@ tcl::namespace::eval punk::args { } } - if {[llength $leadertypelist] == 1} { - set clauseval $ldr + if {$leader_clause_size == 1} { + #set clauseval $ldr + set clauseval [lindex $resultlist 0] } else { set clauseval $resultlist incr ldridx [expr {$consumed - 1}] + + #not quite right.. this sets the -type for all clauses - but they should run independently + #e.g if expr {} elseif 2 {script2} elseif 3 then {script3} (where elseif clause defined as "literal(elseif) expr ?literal(then)? script") + #the elseif 2 {script2} will raise an error because the newtypelist from elseif 3 then {script3} overwrote the newtypelist where then was given the type ?omitted-...? + tcl::dict::set argstate $leadername -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries } @@ -5564,12 +7828,15 @@ tcl::namespace::eval punk::args { #} if {$leadername in $leadernames_received} { tcl::dict::lappend leaders_dict $leadername $clauseval + tcl::dict::lappend argument_clause_typestate $leadername $newtypelist } else { tcl::dict::set leaders_dict $leadername [list $clauseval] + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] } set leadername_multiple $leadername } else { tcl::dict::set leaders_dict $leadername $clauseval + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] set leadername_multiple "" incr nameidx } @@ -5610,7 +7877,19 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- #satisfy test parse_withdef_leaders_no_phantom_default - foreach leadername [dict keys $leaders_dict] { + #foreach leadername [dict keys $leaders_dict] { + # if {[string is integer -strict $leadername]} { + # #ignore leadername that is a positionalidx + # #review - always trailing - could use break? + # continue + # } + # if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { + # #remove the name with empty-string default we used to establish fixed order of names + # #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. + # dict unset leaders_dict $leadername + # } + #} + dict for {leadername _v} $leaders_dict { if {[string is integer -strict $leadername]} { #ignore leadername that is a positionalidx #review - always trailing - could use break? @@ -5624,6 +7903,7 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- + set validx 0 set valname_multiple "" set valnames_received [list] @@ -5649,6 +7929,7 @@ tcl::namespace::eval punk::args { set val [lindex $values $validx] if {$valname ne ""} { set valtypelist [tcl::dict::get $argstate $valname -type] + set clause_size [llength $valtypelist] ;#common case is clause_size == 1 set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] @@ -5656,6 +7937,8 @@ tcl::namespace::eval punk::args { set newtypelist [dict get $assign_d typelist] if {[tcl::dict::get $argstate $valname -optional]} { if {$consumed == 0} { + #error 333 + puts stderr "get_dict cannot assign val:$val to valname:$valname (333)" incr validx -1 set valname_multiple "" incr nameidx @@ -5669,6 +7952,8 @@ tcl::namespace::eval punk::args { set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { + #error 444 + puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" incr validx -1 set valname_multiple "" incr nameidx @@ -5678,8 +7963,9 @@ tcl::namespace::eval punk::args { } #assert can_assign != 0, we have at least one value to assign to clause - if {[llength $valtypelist] == 1} { - set clauseval $val + if {$clause_size == 1} { + #set clauseval $val + set clauseval [lindex $resultlist 0] } else { #clauseval must contain as many elements as the max length of -types! #(empty-string/default for optional (?xxx?) clause members) @@ -5689,10 +7975,11 @@ tcl::namespace::eval punk::args { incr validx [expr {$consumed -1}] if {$validx > [llength $values]-1} { error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valtypelist] ] -argspecs $argspecs]] $msg + set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to $clause_size values." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength $clause_size ] -argspecs $argspecs]] $msg } + #incorrect - we shouldn't update the default. see argument_clause_typestate dict of lists of -type tcl::dict::set argstate $valname -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries } @@ -5705,17 +7992,21 @@ tcl::namespace::eval punk::args { #} if {$valname in $valnames_received} { tcl::dict::lappend values_dict $valname $clauseval + tcl::dict::lappend argument_clause_typestate $valname $newtypelist } else { tcl::dict::set values_dict $valname [list $clauseval] + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] } set valname_multiple $valname } else { tcl::dict::set values_dict $valname $clauseval + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect set valname_multiple "" incr nameidx } lappend valnames_received $valname } else { + #unnamed if {$valname_multiple ne ""} { set valtypelist [tcl::dict::get $argstate $valname_multiple -type] if {[llength $valname_multiple] == 1} { @@ -5764,6 +8055,10 @@ tcl::namespace::eval punk::args { } } #----------------------------------------------------- + #JJJJJJ + #if {[dict size $argument_clause_typestate]} { + # puts ">>>>>[dict get $argspecs id] typestate $argument_clause_typestate" + #} if {$leadermax == -1} { #only check min @@ -5802,6 +8097,7 @@ tcl::namespace::eval punk::args { } #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options + #(and may still contain non-final flag_ident entries such as -increasing|-SORTDIRECTION) #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) @@ -5822,22 +8118,28 @@ tcl::namespace::eval punk::args { # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" #} #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + if {[llength $LEADER_REQUIRED]} { + if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { + set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg + #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + } } - set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] - if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { - set full_missing [dict get $lookup_optset $missing] - set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs + if {[llength $OPT_REQUIRED]} { + set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] + if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { + set full_missing [dict get $lookup_optset $missing] + set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg + #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs + } } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + if {[llength $VAL_REQUIRED]} { + if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { + set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg + #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + } } #--------------------------------------------------------------------------------------------- @@ -5865,20 +8167,58 @@ tcl::namespace::eval punk::args { #check types,ranges,choices set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" + #puts "get_dict>>>>>>>> ---opts_and_values:$opts_and_values" + #puts " >>>>>>> ---lookup_optset :$lookup_optset" #puts "---argstate:$argstate" - tcl::dict::for {api_argname value_group} $opts_and_values { - if {[string match -* $api_argname]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - if {[dict exists $lookup_optset $api_argname]} { - set argname [dict get $lookup_optset $api_argname] + #JJJ argname_or_ident; ident example: -increasing|-SORTOPTION + tcl::dict::for {argname_or_ident value_group} $opts_and_values { + # + #parsekey: key used in resulting leaders opts values dictionaries + # often distinct from the full argname in the ARG_INFO structure + # + if {[string match -* $argname_or_ident]} { + #ident format only applies to options/flags + if {[string first | $argname_or_ident] > -1} { + #flag_ident style (grouped fullname of option with -parsekey) + lassign [split $argname_or_ident |] fullflag parsekey ;#we expect only a single pipe in ident form | + if {[dict exists $lookup_optset $fullflag]} { + set argname [dict get $lookup_optset $fullflag] + #idents should already have correct parsekey + } else { + puts stderr "punk::args::get_dict unable to find $fullflag in $lookup_optset (parsekey:$parsekey) (value_group: $value_group)" + } } else { - puts stderr "unable to find $api_argname in $lookup_optset" + if {[dict exists $lookup_optset $argname_or_ident]} { + #get full option name such as -fg|-foreground from non-alias name such as -foreground + #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined + set argname [dict get $lookup_optset $argname_or_ident] + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #default parsekey: last element in argname without trailing = + set parsekey [string trimright [lindex [split $argname |] end] =] + } + } else { + puts stderr "punk::args::get_dict unable to find $argname_or_ident in $lookup_optset (value_group: $value_group)" + } } } else { - set argname $api_argname + set argname $argname_or_ident + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #leader or value of form x|y has no special meaning and forms the parsekey in entirety by default. + set parsekey $argname + } } + #assert: argname is the key for the relevant argument info in the FORMS//ARG_INFO dict. (here each member available as $argstate) + #argname is usually the full name as specified in the definition: + #e.g -f|-path|--filename= + # (where the parsekey will be by default --filename, possibly overridden by -parsekey value) + #an example argname_or_compound for the above might be: -path|--filename + # where -path is the expanded form of the actual flag used (could have been for example just -p) and --filename is the parsekey set thisarg [tcl::dict::get $argstate $argname] #set thisarg_keys [tcl::dict::keys $thisarg] @@ -5892,22 +8232,55 @@ tcl::namespace::eval punk::args { set defaultval [tcl::dict::get $thisarg -default] } set typelist [tcl::dict::get $thisarg -type] + set clause_size [llength $typelist] set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 set validationtransform [tcl::dict::get $thisarg -validationtransform] #JJJJ + #if {$is_multiple} { + # set vlist $value_group + #} else { + # set vlist [list $value_group] + #} + ##JJJJ + #if {$clause_size == 1} { + # set vlist [list $vlist] + #} + + + #JJ 2025-07-25 + set vlist [list] + #vlist is a list of clauses. Each clause is a list of values of length $clause_size. + #The common case is clause_size 1 - but as we need to treat each clause as a list during validation - we need to list protect the clause when clause_size == 1. if {$is_multiple} { - set vlist $value_group + if {$clause_size == 1} { + foreach c $value_group { + lappend vlist [list $c] + } + } else { + set vlist $value_group + } } else { - set vlist [list $value_group] + if {$clause_size ==1} { + set vlist [list [list $value_group]] + } else { + set vlist [list $value_group] + } } - #JJJJ - if {[llength $typelist] == 1} { - set vlist [list $vlist] + set vlist_typelist [list] + if {[dict exists $argument_clause_typestate $argname]} { + #lookup saved newtypelist (argument_clause_typelist) from can_assign_value result where some optionals were given type ?omitted-? or ?defaulted-? + # args.test: parse_withdef_value_clause_missing_optional_multiple + set vlist_typelist [dict get $argument_clause_typestate $argname] + } else { + foreach v $vlist { + lappend vlist_typelist $typelist + } } + + + set vlist_original $vlist ;#retain for possible final strip_ansi #review - validationtransform @@ -5916,7 +8289,12 @@ tcl::namespace::eval punk::args { package require punk::ansi set vlist_check [list] foreach clause_value $vlist { - lappend vlist_check [punk::ansi::ansistrip $clause_value] + #lappend vlist_check [punk::ansi::ansistrip $clause_value] + set stripped [list] + foreach element $clause_value { + lappend stripped [punk::ansi::ansistrip $element] + } + lappend vlist_check $stripped } } else { #validate_ansistripped 0 @@ -5941,9 +8319,12 @@ tcl::namespace::eval punk::args { set argclass "Unknown argument" } } + set vlist_validate [list] + set vlist_check_validate [list] + set vlist_typelist_validate [list] #reduce our validation requirements by removing values which match defaultval or match -choices #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$api_argname in $receivednames && $has_choices} { + if {$parsekey in $receivednames && $has_choices} { #-choices must also work with -multiple #todo -choicelabels set choiceprefix [tcl::dict::get $thisarg -choiceprefix] @@ -5968,219 +8349,273 @@ tcl::namespace::eval punk::args { #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - set idx 0 ;# + set clause_index -1 ;# #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes #assert llength $vlist == llength [dict get $dname $argname] # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + foreach clause $vlist clause_check $vlist_check clause_typelist $vlist_typelist { + incr clause_index + set element_index -1 ;#element within clause - usually clause size is only 1 + foreach e $clause e_check $clause_check { + incr element_index + set allchoices_in_list 0 + if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { + #vlist and vlist_check can be list of lists if -multiple and -choicemultiple + #each e represents 0 or more choice selections + set c_list $e + set c_check_list $e_check + #todo? check if entire list matches default? + } else { + #only one choice at a time - ensure single entry in c_list c_check_list + set c_list [list $e] + set c_check_list [list $e_check] + } - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- + #----------------------------------- + #fast fail on the wrong number of choices + if {[llength $c_list] < $choicemultiple_min} { + set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg + } + if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { + set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg + } + #----------------------------------- - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail + set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list + foreach c $c_list c_check $c_check_list { + if {$nocase} { + set casemsg " (case insensitive)" + set choices_test [tcl::string::tolower $allchoices] + #Don't lcase the denylist - even in nocase mode! + #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] + set v_test [tcl::string::tolower $c_check] + } else { + set casemsg " (case sensitive)" + set choices_test $allchoices + set v_test $c_check + } + set choice_in_list 0 + set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? + if {!$matches_default} { + if {$choiceprefix} { + #can we handle empty string as a choice? It should just work - REVIEW/test + set choice_exact_match 0 + if {$c_check in $allchoices} { + #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing + set chosen $c_check + set choice_in_list 1 + set choice_exact_match 1 + } elseif {$v_test in $choices_test} { + #assert - if we're here, nocase must be true + #we know choice is present as full-length match except for case + #now we want to select the case from the choice list - not the supplied value + #we don't set choice_exact_match - because we will need to override the optimistic existing val below + #review + foreach avail [lsort -unique $allchoices] { + if {[string match -nocase $c $avail]} { + set chosen $avail + } } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] + #assert chosen will always get set + set choice_in_list 1 + } else { + #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" + #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. + #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. + #in this block we can treat empty result from prefix match as a non-match + if {$nocase} { + #nocase implies that our entered value doesn't have to match case of choices - + #but we would still like to select the best match if there are case-dups. + #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete + # selecting Del will find Delete, del will match delete (and raise error) + # but DEL will also match delete rather than Delete - so again an error is raised. + #This is counterintuitive with -nocase + #This is probably such an edge case that best served with documentation as a feature-not-bug + #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? + #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. + + set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] + #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing + set chosen [lsearch -inline -nocase $allchoices $chosen] + set choice_in_list [expr {$chosen ne ""}] + } else { + set chosen $bestmatch + set choice_in_list 1 + } } else { - set chosen $bestmatch - set choice_in_list 1 + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$chosen eq "" || $chosen in $choiceprefixreservelist} { + set choice_in_list 0 + } else { + set choice_in_list 1 + } } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { + #override choice_in_list if in deny list + #don't allow prefixing for elements from -choiceprefixdenylist + #we still use all elements to calculate the prefixes though + #review - case difference edge cases in choiceprefixdenylist !todo + if {$chosen in $choiceprefixdenylist} { set choice_in_list 0 - } else { - set choice_in_list 1 + set chosen "" } } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all + #override the optimistic existing val + #our existing values in $dname are not list-protected - so we need to check clause_size + if {$choice_in_list && !$choice_exact_match} { + set existing [tcl::dict::get [set $dname] $argname_or_ident] + if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { + #single choice allowed per clause-member + if {$is_multiple} { + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $element_index $chosen + } else { + lset existing $clause_index $element_index $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + #test: choice_multielement_clause + lset existing $element_index $chosen + tcl::dict::set $dname $argname_or_ident $existing + } } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing + if {$is_multiple} { + #puts ">>> existing $existing $choice_idx" + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $clause_index $choice_idx $chosen + } else { + lset existing $clause_index $element_index $choice_idx $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + lset existing $element_index $choice_idx $chosen + tcl::dict::set $dname $argname_or_ident $existing + } } } + } else { + #value as stored in $dname is ok + set choice_in_list [expr {$v_test in $choices_test}] } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] } - } - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" + if {!$choice_in_list && !$matches_default} { + if {!$choicerestricted} { + #if {$is_multiple} { + # set existing [tcl::dict::get [set $dname] $argname] + # lset existing $clause_index $v_test + # tcl::dict::set $dname $argname $existing + #} else { + # tcl::dict::set $dname $argname $v_test + #} + #JJJ + #lappend vlist_validate $c + #lappend vlist_check_validate $c_check } else { - set prefixmsg "" + #unhappy path + if {$choiceprefix} { + set prefixmsg " (or a unique prefix of a value)" + } else { + set prefixmsg "" + } + #review: $c vs $c_check for -badval? + set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg + #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname + } else { + #choice is in list or matches default - no validation for this specific element in the clause + lset clause_typelist $element_index any } + incr choice_idx } - incr choice_idx + + } ;#end foreach e in clause + #jjj 2025-07-16 + #if not all clause_typelist are 'any' + if {[lsearch -not $clause_typelist any] > -1} { + #at least one element still needs validation + lappend vlist_validate $clause + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } - incr idx - } + + } ;#end foreach clause in vlist + #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate + set vlist $vlist_validate + set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate } #todo - don't add to validation lists if not in receivednames - #if we have an optionset such as "-f|-x|-etc" api_argname is -etc - if {$api_argname ni $receivednames} { + #if we have an optionset such as "-f|-x|-etc"; the parsekey is -etc (unless it was overridden by -parsekey in definition) + if {$parsekey ni $receivednames} { set vlist [list] set vlist_check_validate [list] } else { if {[llength $vlist] && $has_default} { - #defaultval here is a value for the clause. - set vlist_validate [list] - set vlist_check_validate [list] - foreach clause_value $vlist clause_check $vlist_check { + #defaultval here is a value for the entire clause. (clause usually length 1) + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + set tp [dict get $thisarg -type] + set clause_size [llength $tp] + foreach clause_value $vlist clause_check $vlist_check clause_typelist $vlist_typelist { #JJJJ - #argname - #thisarg - set tp [dict get $thisarg -type] - if {[llength $tp] == 1} { - if {$clause_value ni $vlist_validate} { - #for -choicemultiple with default that could be a list use 'ni' ?? review + #REVIEW!!! we're inadvertently adding back in things that may have already been decided in choicelist loop as not requiring validation? + if {$clause_value ni $vlist_validate} { + if {$clause_size ==1} { + #for -choicemultiple with default that could be a list use 'ni' + #?? review! if {[lindex $clause_check 0] ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } - } - } else { - if {$clause_value ni $vlist_validate} { + } else { if {$clause_check ne $defaultval} { - lappend vlist_validate $clause_value - lappend vlist_check_validate $clause_check + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } } } + #if {[llength $tp] == 1} { + # if {$clause_value ni $vlist_validate} { + # #for -choicemultiple with default that could be a list use 'ni' + # #?? review! + # if {[lindex $clause_check 0] ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} else { + # if {$clause_value ni $vlist_validate} { + # if {$clause_check ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} #Todo? #else ??? } set vlist $vlist_validate set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate } } @@ -6212,373 +8647,36 @@ tcl::namespace::eval punk::args { #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups #assert [llength $vlist] == [llength $vlist_check] if {[llength $vlist]} { - for {set t 0} {$t < [llength $typelist]} {incr t} { - set typespec [lindex $typelist $t] - set type [string trim $typespec ?] - #puts "$argname - switch on type: $type" - switch -- $type { - any {} - literal { - foreach clause_value $vlist { - set e [lindex $clause_value $t] - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - list { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } + #$t = clause column + + #for {set clausecolumn 0} {$clausecolumn < [llength $typelist]} {incr clausecolumn} {} + set clausecolumn -1 + foreach typespec $typelist { + incr clausecolumn + if {[dict exists $thisarg -typedefaults]} { + set tds [dict get $thisarg -typedefaults] + if {[lindex $vlist $clausecolumn] eq [lindex $tds $clausecolumn]} { + continue } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - if {[regexp [lindex $regexprepass $t] $e]} { - lappend pass_quick_list_e $clauseval - lappend pass_quick_list_e_check $clauseval_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach clauseval $remaining_e clauseval_check $remaining_e_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } + } - if {[tcl::dict::size $thisarg_checks]} { - foreach clauseval $remaining_e_check { - set e_check [lindex $clauseval $t] - if {[dict exists $thisarg_checks -minsize]} { - set minsize [dict get $thisarg_checks -minsize] - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign {} low high ;#set both empty - lassign $range low high - - if {"$low$high" ne ""} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - int { - #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::exists $thisarg -typeranges]} { - set ranges [tcl::dict::get $thisarg -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign $range low high - if {"$low$high" ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - double { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is double -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -typeranges]} { - set ranges [dict get $thisarg_checks -typeranges] - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $range low high - if {$e_check < $low || $e_check > $high} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - bool { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsizes [dict get $thisarg_checks -minsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set minsize [lindex $minsizes $t] - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsizes [dict get $thisarg_checks -maxsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set maxsize [lindex $maxsizes $t] - if {$maxsize ne "-1"} { - if {[tcl::dict::size $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is $type -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[tcl::string::length $e_check] != 1} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } + set type_expression [string trim $typespec ?] + if {$type_expression in {any none}} { + continue } + #puts "$argname - switch on type_expression: $type_expression v:[lindex $vlist $clausecolumn]" + #set typespec [lindex $typelist $clausecolumn] + #todo - handle type-alternates e.g -type char|double + #------------------------------------------------------------------------------------ + #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #------------------------------------------------------------------------------------ + + + #todo - pass validation if matches an entry in -typedefaults + #has_typedefault? + #set typedefault [lindex $typedefaults $clausecolumn] + } @@ -6590,35 +8688,57 @@ tcl::namespace::eval punk::args { if {[tcl::dict::get $thisarg -multiple]} { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict $argname $stripped_list + tcl::dict::set leaders_dict $argname_or_ident $stripped_list } option { - tcl::dict::set opts $argname $stripped_list + tcl::dict::set opts $argname_or_ident $stripped_list } value { - tcl::dict::set values_dict $argname $stripped_list + tcl::dict::set values_dict $argname_or_ident $stripped_list } } } else { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] + tcl::dict::set leaders_dict $argname_or_ident [lindex $stripped_list 0] } option { - tcl::dict::set opts $argname [lindex $stripped_list 0] + tcl::dict::set opts $argname_or_ident [lindex $stripped_list 0] } value { - tcl::dict::set values_dict [lindex $stripped_list 0] + tcl::dict::set values_dict $argname_or_ident [lindex $stripped_list 0] } } } } } - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] + set finalopts [dict create] + dict for {o v} $opts { + if {[string first | $o] > -1} { + #set parsekey [lindex [split $o |] end] + dict set finalopts [lindex [split $o |] end] $v + } else { + dict set finalopts $o $v + } + } + return [tcl::dict::create leaders $leaders_dict opts $finalopts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] } - + lappend PUNKARGS [list { + @id -id ::punk::args::forms + @cmd -name punk::args::forms\ + -summary\ + "List command forms."\ + -help\ + "Return names for each form of a command identified by 'id'. + Most commands are single-form and will only return the name '_default'." + @leaders -min 0 -max 0 + @opts + @values -min 1 -max 1 + id -multiple 0 -optional 0 -help\ + "Exact id of command" + }] proc forms {id} { set spec [get_spec $id] if {[dict size $spec]} { @@ -6627,15 +8747,46 @@ tcl::namespace::eval punk::args { return [list] } } + + + lappend PUNKARGS [list { + @id -id ::punk::args::eg + @cmd -name punk::args::eg\ + -summary\ + "Command examples."\ + -help\ + "Return command examples from -help in @examples + directive of a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + }] + proc eg {args} { + set argd [punk::args::parse $args withid ::punk::args::eg] + lassign [dict values $argd] leaders opts values received + set cmditems [dict get $values cmditem] + set id [lindex $cmditems 0] + set cmdargs [lrange $cmditems 1 end] + + set spec [get_spec $id] + if {$spec eq ""} { + return + } + return [dict get $spec examples_info -help] + } + lappend PUNKARGS [list { @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id + @cmd -name punk::args::synopsis\ + -summary\ + "Command synopsis"\ + -help\ + "Return synopsis for each form of a command on separate lines. If -form is given, supply only the synopsis for that form. " @opts + -noheader -type none -form -type string -default * -return -type string -default full -choices {full summary dict} @values -min 1 -max -1 @@ -6650,13 +8801,18 @@ tcl::namespace::eval punk::args { set has_punkansi 1 } if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set NI [punk::ansi::a+ noitalic] + set I [punk::ansi::a+ italic] + set NI [punk::ansi::a+ noitalic] + #for inner question marks marking optional type + set IS [punk::ansi::a+ italic strike] + set NIS [punk::ansi::a+ noitalic nostrike] #set RST [punk::ansi::a] set RST "\x1b\[m" } else { set I "" set NI "" + set IS "" + set NIS "" set RST "" } @@ -6673,12 +8829,12 @@ tcl::namespace::eval punk::args { ##set id [lindex $arglist 0] ##set cmdargs [lrange $arglist 1 end] - lassign [dict values $argd] leaders opts values + lassign [dict values $argd] leaders opts values received set form [dict get $opts -form] set opt_return [dict get $opts -return] set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] + set id [lindex $cmditems 0] + set cmdargs [lrange $cmditems 1 end] set spec [get_spec $id] @@ -6704,11 +8860,15 @@ tcl::namespace::eval punk::args { } set SYND [dict create] - set syn "" + dict set SYND cmd_info [dict get $spec cmd_info] + #leading "# " required (punk::ns::synopsis will pass through) + if {![dict exists $received -noheader]} { + set syn "# [Dict_getdef $spec cmd_info -summary ""]\n" + } #todo - -multiple etc foreach f $form_names { set SYNLIST [list] - dict set SYND $f [list] + dict set SYND FORMS $f [list] append syn "$id" set forminfo [dict get $spec FORMS $f] #foreach argname [dict get $forminfo LEADER_NAMES] { @@ -6731,31 +8891,69 @@ tcl::namespace::eval punk::args { # dict set ARGD display $display # dict lappend SYND $f $ARGD #} + set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] set typelist [dict get $arginfo -type] if {[llength $typelist] == 1} { set tp [lindex $typelist 0] - if {[dict exists $arginfo -typesynopsis]} { + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { #set arg_display [dict get $arginfo -typesynopsis] - set clause [dict get $arginfo -typesynopsis] + set clause $ts } else { #set arg_display $argname - if {$tp eq "literal"} { - set clause [lindex $argname end] - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set clause $match - } else { - set clause $I$argname$NI + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + set type_alternatives [_split_type_expression $tp] + foreach tp_alternative $type_alternatives { + set firstword [lindex $tp_alternative 0] + switch -exact -- $firstword { + literal { + set match [lindex $tp_alternative 1] + lappend alternates $match + } + literalprefix { + #todo - trie styling on prefix calc + set match [lindex $tp_alternative 1] + lappend alternates $match + } + stringstartswith { + set match [lindex $tp_alternative 1] + lappend alternates $match* + } + stringendswith { + set match [lindex $tp_alternative 1] + lappend alternates *$match + } + default { + lappend alternates $I$argname$NI + } + } + + #if {$tp_alternative eq "literal"} { + # lappend alternates [lindex $argname end] + #} elseif {[string match literal(*) $tp_alternative]} { + # set match [string range $tp_alternative 8 end-1] + # lappend alternates $match + #} elseif {[string match literalprefix(*) $tp_alternative]} { + # set match [string range $tp_alternative 14 end-1] + # lappend alternates $match + #} else { + # lappend alternates $I$argname$NI + #} } + #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) + #todo - trie prefixes display + set alternates [punk::args::lib::lunique $alternates] + set clause [join $alternates |] } } else { set n [expr {[llength $typelist]-1}] set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types set clause "" - if {[dict exists $arginfo -typesynopsis]} { - set tp_displaylist [dict get $arginfo -typesynopsis] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts } else { set tp_displaylist [lrepeat [llength $typelist] ""] } @@ -6824,88 +9022,162 @@ tcl::namespace::eval punk::args { dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } foreach argname [dict get $forminfo OPT_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] set ARGD [dict create argname $argname class option] set tp [dict get $arginfo -type] - if {[dict exists $arginfo -typesynopsis]} { - set tp_display [dict get $arginfo -typesynopsis] + if {$tp eq "none"} { + #assert - argname may have aliases delimited by | - but no aliases end with = + #(disallowed in punk::args::define) + set argdisplay $argname } else { - #set tp_display "<$tp>" - set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_member [split $tp |] { - #-type literal not valid for opt - review - if {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] - lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] - lappend alternates $match + #assert [llength $tp] == 1 (multiple values for flag unspported in punk::args::define) + if {[string match {\?*\?} $tp]} { + set tp [string range $tp 1 end-1] + set value_is_optional true + } else { + set value_is_optional false + } + + + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_display $ts + #user may or may not have remembered to match the typesynopsis with the optionality by wrapping with ? + #review - if user wrapped with ?*? and also leading/trailing ANSI - we won't properly strip + #todo - enforce no wrapping '?*?' in define for -typesynopsis? + set tp_display [string trim $tp_display ?] + } else { + + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + foreach tp_alternative [split $tp |] { + #-type literal not valid for opt - review + if {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] + lappend alternates $match + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] + lappend alternates $match + } else { + lappend alternates <$I$tp_alternative$NI> + } + } + #todo - trie prefixes display? + set alternates [punk::args::lib::lunique $alternates] + set tp_display [join $alternates |] + } + if {[string first | $tp_display] >=0} { + #need to bracket alternate-types to distinguish pipes delimiting flag aliases + set tp_display "($tp_display)" + } + set argdisplay "" + foreach aliasflag [split $argname |] { + if {[string match --* $aliasflag]} { + if {[string index $aliasflag end] eq "="} { + set alias [string range $aliasflag 0 end-1] + if {$value_is_optional} { + append argdisplay "$alias$IS?$NIS=$tp_display$IS?$NIS|" + } else { + append argdisplay "$alias=$tp_display|" + } + } else { + if {$value_is_optional} { + append argdisplay "$aliasflag $IS?$NIS$tp_display$IS?$NIS|" + } else { + append argdisplay "$aliasflag $tp_display|" + } + } } else { - lappend alternates $I<$tp_member>$NI + if {$value_is_optional} { + #single dash flag can't accept optional value + append argdisplay "$aliasflag|" + } else { + append argdisplay "$aliasflag $tp_display|" + } } } - #todo - trie prefixes display? - set alternates [punk::args::lib::lunique $alternates] - set tp_display [join $alternates |] + set argdisplay [string trimright $argdisplay |] } if {[dict get $arginfo -optional]} { if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname $tp_display?..." - } + set display "?$argdisplay?..." } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname $tp_display?" - } + set display "?$argdisplay?" } } else { if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname $tp_display ?$argname $tp_display?..." - } + set display "$argdisplay ?$argdisplay?..." } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname $tp_display" - } + set display $argdisplay } } + + #if {[string index $argname end] eq "="} { + # set __ "" + #} else { + # set __ " " + #} + #if {[dict get $arginfo -optional]} { + # if {[dict get $arginfo -multiple]} { + # if {$tp eq "none"} { + # set display "?$argname?..." + # } else { + # set display "?$argname$__$tp_display?..." + # } + # } else { + # if {$tp eq "none"} { + # set display "?$argname?" + # } else { + # set display "?$argname$__$tp_display?" + # } + # } + #} else { + # if {[dict get $arginfo -multiple]} { + # if {$tp eq "none"} { + # set display "$argname ?$argname...?" + # } else { + # set display "$argname$__$tp_display ?$argname$__$tp_display?..." + # } + # } else { + # if {$tp eq "none"} { + # set display $argname + # } else { + # set display "$argname$__$tp_display" + # } + # } + #} append syn " $display" dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] set typelist [dict get $arginfo -type] if {[llength $typelist] == 1} { set tp [lindex $typelist 0] - if {[dict exists $arginfo -typesynopsis]} { + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { #set arg_display [dict get $arginfo -typesynopsis] - set clause [dict get $arginfo -typesynopsis] + set clause $ts } else { #set arg_display $argname set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - foreach tp_member [split $tp |] { - if {$tp_member eq "literal"} { + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { lappend alternates [lindex $argname end] - } elseif {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] lappend alternates $match } else { lappend alternates $I$argname$NI @@ -6920,8 +9192,9 @@ tcl::namespace::eval punk::args { set n [expr {[llength $typelist]-1}] set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types set clause "" - if {[dict exists $arginfo -typesynopsis]} { - set tp_displaylist [dict get $arginfo -typesynopsis] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts } else { set tp_displaylist [lrepeat [llength $typelist] ""] } @@ -6937,14 +9210,14 @@ tcl::namespace::eval punk::args { } #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - foreach tp_member [split $tp |] { - if {$tp_member eq "literal"} { + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { lappend alternates $elementname - } elseif {[string match literal(*) $tp_member]} { - set match [string range $tp_member 8 end-1] + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] lappend alternates $match - } elseif {[string match literalprefix(*) $tp_member]} { - set match [string range $tp_member 14 end-1] + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] lappend alternates $match } else { if {$td eq ""} { @@ -6999,9 +9272,11 @@ tcl::namespace::eval punk::args { dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } append syn \n + dict set SYND FORMS $f $FORMARGS } switch -- $opt_return { full { @@ -7009,8 +9284,8 @@ tcl::namespace::eval punk::args { } summary { set summary "" - showdict $SYND - dict for {form arglist} $SYND { + set FORMS [dict get $SYND FORMS] + dict for {form arglist} $FORMS { append summary $id set class_state leader set option_count 0 @@ -7150,6 +9425,26 @@ tcl::namespace::eval punk::args::lib { #[para] Secondary functions that are part of the API #[list_begin definitions] + #tcl86 compat for string is dict - but without -strict or -failindex options + if {[catch {string is dict {}} errM]} { + proc string_is_dict {args} { + #ignore opts + set str [lindex $args end] + if {[catch {[llength $str] len}]} { + return 0 + } + if {$len % 2 == 0} { + return 1 + } + return 0 + } + } else { + proc string_is_dict {args} { + string is dict {*}$args + } + } + + #proc utility1 {p1 args} { # #*** !doctools # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] @@ -7218,7 +9513,10 @@ tcl::namespace::eval punk::args::lib { #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} lappend PUNKARGS [list { @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ + @cmd -name punk::args::lib::tstr\ + -summary\ + "Templating with \$\{$varName\}"\ + -help\ "A rough equivalent of js template literals Substitutions: @@ -7950,7 +10248,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.1.9 + set version 0.2 }] return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm index 3a5f25b0..8d5a5dca 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/config-0.1.tm @@ -449,7 +449,7 @@ tcl::namespace::eval punk::config { Accepts globs eg XDG*" @leaders -min 1 -max 1 #todo - load more whichconfig choices? - whichconfig -type string -choices {config startup-configuration running-configuration} + whichconfig -type any -choices {config startup-configuration running-configuration} @values -min 0 -max -1 globkey -type string -default * -optional 1 -multiple 1 }] @@ -495,7 +495,7 @@ tcl::namespace::eval punk::config { @cmd -name punk::config::configure -help\ "Get/set configuration values from a config" @leaders -min 1 -max 1 - whichconfig -type string -choices {defaults startup-configuration running-configuration} + whichconfig -type any -choices {defaults startup-configuration running-configuration} @values -min 0 -max 2 key -type string -optional 1 newvalue -optional 1 diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm index 19d9d7e4..4322ceaa 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/console-0.1.1.tm @@ -612,10 +612,12 @@ namespace eval punk::console { -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" - -expected_ms -default 100 -type integer -help\ + -expected_ms -default 300 -type integer -help\ "Expected number of ms for response from terminal. 100ms is usually plenty for a local terminal and a - basic query such as cursor position." + basic query such as cursor position. + However on a busy machine a higher timeout may be + prudent." @values -min 2 -max 2 query -type string -help\ "ANSI sequence such as \x1b\[?6n which @@ -680,19 +682,21 @@ namespace eval punk::console { upvar ::punk::console::ansi_response_timeoutid timeoutid set accumulator($callid) "" set waitvar($callid) "" - + lappend queue $callid if {[llength $queue] > 1} { #while {[lindex $queue 0] ne $callid} {} set queuedata($callid) $args set runningid [lindex $queue 0] - while {$runningid ne $callid} { + while {$runningid ne $callid} { + #puts stderr "." vwait ::punk::console::ansi_response_wait set runningid [lindex $queue 0] if {$runningid ne $callid} { set ::punk::console::ansi_response_wait($runningid) $::punk::console::ansi_response_wait($runningid) update ;#REVIEW - probably a bad idea after 10 + set runningid [lindex $queue 0] ;#jn test } } } @@ -779,7 +783,7 @@ namespace eval punk::console { puts "blank extension $waitvar($callid)" puts "->[set $waitvar($callid)]<-" } - puts stderr "get_ansi_response_payload Extending timeout by $extension" + puts stderr "get_ansi_response_payload Extending timeout by $extension for callid:$callid" after cancel $timeoutid($callid) set total_elapsed [expr {[clock millis] - $tslaunch($callid)}] set last_elapsed [expr {[clock millis] - $lastvwait}] @@ -916,7 +920,8 @@ namespace eval punk::console { unset -nocomplain tslaunch($callid) dict unset queuedata $callid - lpop queue 0 + #lpop queue 0 + ledit queue 0 0 if {[llength $queue] > 0} { set next_callid [lindex $queue 0] set waitvar($callid) go_ahead @@ -977,7 +982,7 @@ namespace eval punk::console { set tsnow [clock millis] set total_elapsed [expr {[set tslaunch($callid)] - $tsnow}] set last_elapsed [expr {[set tsclock($callid)] - $tsnow}] - if {[string length $chunks($callid)] % 10 == 0 || $last_elapsed > 16} { + if {[string length $sofar] % 10 == 0 || $last_elapsed > 16} { if {$total_elapsed > 3000} { #REVIEW #too long since initial read handler launched.. @@ -1239,7 +1244,7 @@ namespace eval punk::console { lappend PUNKARGS [list { @id -id ::punk::console::show_input_response @cmd -name punk::console::show_input_response -help\ - "" + "Debug command for console queries using ANSI" @opts -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" @@ -1247,9 +1252,9 @@ namespace eval punk::console { "Number of ms to wait for response" @values -min 1 -max 1 request -type string -help\ - "ANSI sequence such as \x1b\[?6n which + {ANSI sequence such as \x1b\[?6n which should elicit a response by the terminal - on stdin" + on stdin} }] proc show_input_response {args} { set argd [punk::args::parse $args withid ::punk::console::show_input_response] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm index 7d1375d7..a95a6242 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/du-0.1.0.tm @@ -70,6 +70,7 @@ namespace eval punk::du { proc du { args } { variable has_twapi + #todo - use punk::args if 0 { switch -exact [llength $args] { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm index 5532ed33..6ce76618 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm @@ -301,6 +301,7 @@ tcl::namespace::eval punk::lib::compat { if {"::lpop" ne [info commands ::lpop]} { #puts stderr "Warning - no built-in lpop" interp alias {} lpop {} ::punk::lib::compat::lpop + punk::args::set_alias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore } proc lpop {lvar args} { #*** !doctools @@ -339,6 +340,51 @@ tcl::namespace::eval punk::lib::compat { set l $newlist return $v } + if {"::ledit" ni [info commands ::ledit]} { + interp alias {} ledit {} ::punk::lib::compat::ledit + punk::args::set_alias ::punk::lib::compat::ledit ::ledit + } + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [punk::lib::lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [punk::lib::lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } #slight isolation - varnames don't leak - but calling context vars can be affected @@ -695,14 +741,15 @@ namespace eval punk::lib { proc lswap {lvar a z} { upvar $lvar l - if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { + set len [llength $l] + if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { #lindex_resolve_basic returns only -1 if out of range #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred #(e.g using: lswap mylist end-2 end on a two element list) #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $l $a] + set a_index [lindex_resolve $len $a] set a_msg "" switch -- $a_index { -2 { @@ -712,7 +759,7 @@ namespace eval punk::lib { set a_msg "1st supplied index $a is below the lower bound for the list (0)" } } - set z_index [lindex_resolve $l $z] + set z_index [lindex_resolve $len $z] set z_msg "" switch -- $z_index { -2 { @@ -1100,7 +1147,7 @@ namespace eval punk::lib { - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segement in each pattern switches to a dict operation to retrieve the value by key. + The second level segment in each pattern switches to a dict operation to retrieve the value by key. When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. } }] @@ -1137,11 +1184,13 @@ namespace eval punk::lib { if {!$has_punk_ansi} { set RST "" set sep " = " - set sep_mismatch " mismatch " + #set sep_mismatch " mismatch " + set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) } else { set RST [punk::ansi::a] set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " } package require punk::pipe #package require punk ;#we need pipeline pattern matching features @@ -1173,6 +1222,7 @@ namespace eval punk::lib { -keysortdirection -default increasing -choices {increasing decreasing} -debug -default 0 -type boolean -help\ "When enabled, produces some rudimentary debug output on stderr" + -- -type none -optional 1 @values -min 1 -max -1 dictvalue -type list -help\ "dict or list value" @@ -1465,7 +1515,7 @@ namespace eval punk::lib { if {![regexp $re_idxdashidx $p _match a b]} { error "unrecognised pattern $p" } - set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high + set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds if {${lower_resolve} == -2} { ##x @@ -1478,7 +1528,7 @@ namespace eval punk::lib { } else { set lower $lower_resolve } - set upper [punk::lib::lindex_resolve $dval $b] + set upper [punk::lib::lindex_resolve [llength $dval] $b] if {$upper == -3} { ##x #upper bound is below list range - @@ -1831,7 +1881,8 @@ namespace eval punk::lib { if {$last_hidekey} { append result \n } - append result [textblock::join_basic -- $kblock $sblock $vblock] \n + #append result [textblock::join_basic -- $kblock $sblock $vblock] \n + append result [textblock::join_basic_raw $kblock $sblock $vblock] \n } set last_hidekey $hidekey incr kidx @@ -1880,6 +1931,19 @@ namespace eval punk::lib { } proc is_list_all_in_list {small large} { + if {[llength $small] > [llength $large]} {return 0} + foreach x $large { + ::set ($x) {} + } + foreach x $small { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } + #v2 generally seems slower + proc is_list_all_in_list2 {small large} { set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] return [struct::list equal [lsort $small] $small_in_large] } @@ -1888,11 +1952,22 @@ namespace eval punk::lib { package require struct::list package require struct::set } - append body [info body is_list_all_in_list] - proc is_list_all_in_list {small large} $body + append body [info body is_list_all_in_list2] + proc is_list_all_in_list2 {small large} $body } - proc is_list_all_ni_list {a b} { + proc is_list_all_ni_list {A B} { + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {[info exists ($x)]} { + return 0 + } + } + return 1 + } + proc is_list_all_ni_list2 {a b} { set i [struct::set intersect $a $b] return [expr {[llength $i] == 0}] } @@ -1900,8 +1975,8 @@ namespace eval punk::lib { set body { package require struct::list } - append body [info body is_list_all_ni_list] - proc is_list_all_ni_list {a b} $body + append body [info body is_list_all_ni_list2] + proc is_list_all_ni_list2 {a b} $body } #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist @@ -1917,7 +1992,16 @@ namespace eval punk::lib { } return $result } + #with ledit (also avail in 8.6 using punk::lib::compat::ledit proc ldiff2 {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + foreach item $removeitems { + set posns [lsearch -all -exact $fromlist $item] + foreach p $posns {ledit fromlist $p $p} + } + return $fromlist + } + proc ldiff3 {fromlist removeitems} { set doomed [list] foreach item $removeitems { lappend doomed {*}[lsearch -all -exact $fromlist $item] @@ -2158,35 +2242,75 @@ namespace eval punk::lib { } } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bunds on upper vs lower side - proc lindex_resolve {list index} { + # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side + #REVIEW: This shouldn't really need the list itself - just the length would suffice + punk::args::define { + @id -id ::punk::lib::lindex_resolve + @cmd -name punk::lib::lindex_resolve\ + -summary\ + "Resolve an indexexpression to an integer based on supplied list or string length."\ + -help\ + "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 + to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating + whether the index was below or above the range of possible indices for the length supplied. + + Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. + This means the proc may be called with something like $x+2 end-$y etc + Sometimes the actual integer index is desired. + + We want to resolve the index used, without passing arbitrary expressions into the 'expr' function + - which could have security risks. + lindex_resolve will parse the index expression and return: + a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) + b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) + lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote + out of range at either end of the list/string. + Otherwise it will return an integer corresponding to the position in the data. + This is in stark contrast to Tcl list/string function indices which will return empty strings for out of + bounds indices, or in the case of lrange, return results anyway. + Like Tcl list commands - it will produce an error if the form of the index is not acceptable. + For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side + - thus returning -2 + + Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. + We will get something like 10+1 - which can be resolved safely with expr + " + @values -min 2 -max 2 + datalength -type integer + index -type indexexpression + } + proc lindex_resolve {len index} { #*** !doctools - #[call [fun lindex_resolve] [arg list] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list - #[para]Users may define procs which accept a list index and wish to accept the forms understood by Tcl. + #[call [fun lindex_resolve] [arg len] [arg index]] + #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length + #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. #[para]This means the proc may be called with something like $x+2 end-$y etc #[para]Sometimes the actual integer index is desired. #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. #[para]lindex_resolve will parse the index expression and return: #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list + #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out or bounds indices, or in the case of lrange, return results anyway. + #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which we will resolve (hopefully safely) with expr + #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr #if {![llength $list]} { # #review # return ??? #} + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } else { #integer may still have + sign - normalize with expr @@ -2203,7 +2327,7 @@ namespace eval punk::lib { } } else { #index is 'end' - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { #special case - 'end' with empty list - treat end like a positive number out of bounds return -2 @@ -2212,7 +2336,7 @@ namespace eval punk::lib { } } if {$offset == 0} { - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { return -2 ;#special case as above } else { @@ -2220,7 +2344,7 @@ namespace eval punk::lib { } } else { #by now, if op = + then offset = 0 so we only need to handle the minus case - set index [expr {([llength $list]-1) - $offset}] + set index [expr {($len-1) - $offset}] } if {$index < 0} { return -3 @@ -2245,33 +2369,32 @@ namespace eval punk::lib { } if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } return $index } } } - proc lindex_resolve_basic {list index} { + proc lindex_resolve_basic {len index} { #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] + #[call [fun lindex_resolve_basic] [arg len] [arg index]] #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) #[para] returns -1 for out of range at either end, or a valid integer index #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command + #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 #[para] For pure integer indices the performance should be equivalent - #set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ - # - which - #for {set i 0} {$i < [llength $list]} {incr i} { - # lappend indices $i - #} + if {![string is integer -strict $len]} { + error "lindex_resolve_basic len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { + if {$index < 0 || ($index >= $len)} { #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. return -1 } else { @@ -2279,13 +2402,15 @@ namespace eval punk::lib { return [expr {$index}] } } - if {[llength $list]} { - set indices [punk::lib::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) + if {$len > 0} { + #For large len - this is a wasteful allocation if no true lseq available in Tcl version. + #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) + set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. } else { - set indices [list] + set testlist [list] + #we want to call 'lindex' even in this case - to get the appropriate error message } - set idx [lindex $indices $index] + set idx [lindex $testlist $index] if {$idx eq ""} { #we have no way to determine if out of bounds is at lower vs upper end return -1 @@ -2304,6 +2429,81 @@ namespace eval punk::lib { } } + proc string_splitbefore {str index} { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + return [list $str ""] + } + -3 { + return [list "" $str] + } + } + } + return [list [string range $str 0 $index-1] [string range $str $index end]] + #scan %s stops at whitespace - not useful here. + #scan $s %${p}s%s + } + proc string_splitbefore_indices {str args} { + set parts [list $str] + set sizes [list [string length $str]] + set s 0 + foreach index $args { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + -3 { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + } + } + if {$index <= 0} { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + if {$index >= [string length $str]} { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + set i -1 + set a 0 + foreach sz $sizes { + incr i + if {$a + $sz > $index} { + set p [lindex $parts $i] + #puts "a:$a index:$index" + if {$a == $index} { + break + } + ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] + ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] + break + } + incr a $sz + } + #puts "->parts:$parts" + #puts "->sizes:$sizes" + } + return $parts + } proc K {x y} {return $x} #*** !doctools @@ -3133,8 +3333,7 @@ namespace eval punk::lib { #Each resulting line should have a reset of some type at start and a pure-reset at end to stop #see if we can find an ST sequence that most terminals will not display for marking sections? if {$opt_ansireplays} { - #package require punk::ansi - + ;#package require punk::ansi if {$opt_ansiresets} { set RST "\x1b\[0m" } else { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index 6f01e340..19d5177d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -81,14 +81,15 @@ tcl::namespace::eval punk::libunknown { }] variable epoch - if {![info exists epoch]} { - set tmstate [dict create 0 {}] - set pkgstate [dict create 0 {}] - set tminfo [dict create current 0 epochs $tmstate] - set pkginfo [dict create current 0 epochs $pkgstate] + #if {![info exists epoch]} { + # set tmstate [dict create 0 {}] + # set pkgstate [dict create 0 {}] + # set tminfo [dict create current 0 epochs $tmstate] + # set pkginfo [dict create current 0 epochs $pkgstate] + + # set epoch [dict create tm $tminfo pkg $pkginfo] + #} - set epoch [dict create tm $tminfo pkg $pkginfo] - } variable has_package_files if {[catch {package files foobaz}]} { @@ -114,7 +115,19 @@ tcl::namespace::eval punk::libunknown { # Import the pattern used to check package names in detail. variable epoch set pkg_epoch [dict get $epoch tm current] - + set must_scan 0 + if {[dict exists $epoch tm untracked $name]} { + set must_scan 1 + #a package that was in the package database at the start - is now being searched for as unknown + #our epoch info is not reliable for pre-known packages - so increment the epoch and fully clear the 'added' paths even in zipfs to do proper scan + + #review + #epoch_incr_pkg clearadded + #epoch_incr_tm clearadded + #puts ">>>> removing untracked tm: $name" + dict unset epoch tm untracked $name + #whie it is not the most common configuration - a package could be provided both as a .tm and by packageIndex.tcl files + } #variable paths upvar ::tcl::tm::paths paths @@ -151,7 +164,8 @@ tcl::namespace::eval punk::libunknown { if {![interp issafe] && ![file exists $path]} { continue } - set currentsearchpath [file join $path $pkgroot] + set currentsearchpath $path + set specificsearchpath [file join $path $pkgroot] # Get the module files out of the subdirectories. # - Safe Base interpreters have a restricted "glob" command that @@ -162,32 +176,35 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $path]} { - if {[dict exists $epoch tm epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch tm epochs $pkg_epoch indexes $specificsearchpath]} { #indexes are actual .tm files here - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles ( cached ): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" } else { - if {![interp issafe] && ![file exists $currentsearchpath]} { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + if {![interp issafe] && ![file exists $specificsearchpath]} { + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] continue } - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] # ################################################################# if {$has_zipfs && [string match $zipfsroot* $path]} { + #The entire tm tre is available so quickly from the zipfs::list call - that we can gather all at once. set tmfiles [::tcl::zipfs::list $currentsearchpath/*.tm] ;#could theoretically be a dir - this is effectively a tree traversal foreach tm_path $tmfiles { dict set epoch tm epochs $pkg_epoch indexes [file dirname $tm_path] $tm_path $pkg_epoch } - #retrieval using tcl::zipfs::list got (and cached) extras - limit to currentsearchpath - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + #retrieval using tcl::zipfs::list got (and cached) extras - limit to specificsearchpath + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] } else { - set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + #set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + set tmfiles [glob -nocomplain -directory $specificsearchpath *.tm] foreach tm_path $tmfiles { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + #dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath $tm_path $pkg_epoch } } #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles (UNcached): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" @@ -203,8 +220,8 @@ tcl::namespace::eval punk::libunknown { set can_skip_update 0 if {[string match $zipfsroot* $path]} { #static tm location - if {[dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath $name]} { + if {[dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath]} { + if {![dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath $name]} { #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP orig:$original name:$name args:$args searchpath:$currentsearchpath" set can_skip_update 1 @@ -213,19 +230,13 @@ tcl::namespace::eval punk::libunknown { #dict unset epoch tm epochs $pkg_epoch added $currentsearchpath $name } } - } else { - #dynamic - can only skip if negatively cached for the current epoch - if {[dict exists $epoch tm epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_update 1 - } - } + if {!$can_skip_update} { set strip [llength [file split $path]] set found_name_in_currentsearchpath 0 ;#for negative cache by epoch - catch { + if {[catch { foreach file $tmfiles { set pkgfilename [join [lrange [file split $file] $strip end] ::] @@ -252,6 +263,20 @@ tcl::namespace::eval punk::libunknown { # the one we already have. # This does not apply to Safe Base interpreters because # the token-to-directory mapping may have changed. + + #JMN - review. + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } + if {$pkgname eq $name} { + #can occur multiple times, different versions + #record package name as found in this path whether version satisfies or not + set found_name_in_currentsearchpath 1 + } + #don't override the ifneeded script - for tm files the first encountered 'wins'. continue } @@ -273,8 +298,15 @@ tcl::namespace::eval punk::libunknown { "[::list package provide $pkgname $pkgversion];[::list source $file]" #JMN - #store only once for each name, although there may be multiple versions - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkg_epoch + #store only once for each name, although there may be multiple versions of same package within this searchpath + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. + #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } # We abort in this unknown handler only if we got a # satisfying candidate for the requested package. @@ -298,10 +330,8 @@ tcl::namespace::eval punk::libunknown { set found_name_in_currentsearchpath 1 } } - } - if {!$found_name_in_currentsearchpath} { - #can record as unfound for this path - for this epoch - dict set epoch tm epochs $pkg_epoch unfound $currentsearchpath $name 1 + } errMsg]} { + puts stderr "zipfs_tm_Unknownhandler: error for tm file $file searchpath:$currentsearchpath" } } @@ -380,9 +410,9 @@ tcl::namespace::eval punk::libunknown { } - if {$satisfied} { - ##return - } + #if {$satisfied} { + # return + #} } # Fallback to previous command, if existing. See comment above about @@ -399,6 +429,25 @@ tcl::namespace::eval punk::libunknown { variable epoch set pkg_epoch [dict get $epoch pkg current] + #review - the ifneeded script is not the only thing required in a new interp.. consider tclIndex files and auto_load mechanism. + #also the pkgIndex.tcl could possibly provide a different ifneeded script based on interp issafe (or other interp specific things?) + #if {[dict exists $epoch scripts $name]} { + # set vscripts [dict get $epoch scripts $name] + # dict for {v scr} $vscripts { + # puts ">package ifneeded $name $v" + # package ifneeded $name $v $scr + # } + # return + #} + set must_scan 0 + if {[dict exists $epoch pkg untracked $name]} { + #a package that was in the package database at the start - is now being searched for as unknown + #(due to a package forget?) + #our epoch info is not valid for pre-known packages - so setting must_scan to true + set must_scan 1 + #puts ">>>> removing pkg untracked: $name" + dict unset epoch pkg untracked $name + } #global auto_path env global auto_path @@ -414,7 +463,7 @@ tcl::namespace::eval punk::libunknown { set zipfsroot [tcl::zipfs::root] set has_zipfs 1 } else { - set zipfsroot "//zipfs:/" ;#doesn't matter much what we use here - don't expect in tm list if no zipfs commands + set zipfsroot "//zipfs:/" ;#doesn't matter too much what we use here - don't expect in tm list if no zipfs commands set has_zipfs 0 } @@ -425,6 +474,14 @@ tcl::namespace::eval punk::libunknown { #question is whether some pkgIndex.tcl files may do a package forget? They probably don't/shouldn't(?) Does that matter here anyway? set before_dict [dict create] + #J2 + #siblings that have been affected by source scripts - need to retest ifneeded scripts at end for proper ordering. + set refresh_dict [dict create] + + #Note that autopath is being processed from the end to the front + #ie last lappended first. This means if there are duplicate versions earlier in the list, + #they will be the last to call 'package provide' for that version and so their provide script will 'win'. + #This means we should have faster filesystems such as zipfs earlier in the list. # Cache the auto_path, because it may change while we run through the # first set of pkgIndex.tcl files @@ -432,6 +489,7 @@ tcl::namespace::eval punk::libunknown { while {[llength $use_path]} { set dir [lindex $use_path end] + # Make sure we only scan each directory one time. if {[info exists tclSeenPath($dir)]} { set use_path [lrange $use_path 0 end-1] @@ -449,7 +507,7 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $dir]} { set currentsearchpath $dir - if {[dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { set indexfiles [dict keys [dict get $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]] #puts stderr "--->zipfs_tclPkgUnknown $tid llength tmfiles ( cached ): [format %4d [llength $indexfiles]] name:$name searchpath:$currentsearchpath" } else { @@ -468,29 +526,26 @@ tcl::namespace::eval punk::libunknown { } set can_skip_sourcing 0 - if {$has_zipfs && [string match $zipfsroot* $dir]} { + #if {$has_zipfs && [string match $zipfsroot* $dir]} { #static auto_path dirs - #can avoid scan if added via this path in any epoch - if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" - set can_skip_sourcing 1 - } else { - #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? - #remove it and let it be readded if it's still provided by this path? - #probably doesn't make sense for static path? - #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + if {!$must_scan} { + if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { + if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { + #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. + #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" + set can_skip_sourcing 1 + } else { + #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? + #remove it and let it be readded if it's still provided by this path? + #probably doesn't make sense for static path? + #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + } } } - } else { - #dynamic auto_path dirs - libs could have been added/removed - #scan unless cached negative for this epoch - if {[dict exists $epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_sourcing 1 - } - } + #} + + + #An edge case exception is that after a package forget, a deliberate call to 'package require non-existant' #will not trigger rescans for all versions of other packages. #A rescan of a specific package for all versions can still be triggered with a package require for @@ -498,33 +553,53 @@ tcl::namespace::eval punk::libunknown { #(or misordered min max e.g package require md5 1-0 i.e a deliberately unsatisfiable version range) set sourced 0 + set just_added [dict create] + set just_changed [dict create] + #set sourced_files [list] + + #J2 + #set can_skip_sourcing 0 + if {!$can_skip_sourcing} { #Note - naive comparison of before_pkgs vs after_pkgs isn't quite enough to tell if something was added. It could have added a version. #this will stop us rescanning everything properly by doing a 'package require nonexistant' - #use 'info exists' to only call package names once and then append? worth it? + #use 'info exists' to only call package names once and then append? + #This could be problematic? (re-entrant tclPkgUnknown in some pkgIndex scripts?) pkgIndex.tcl scripts "shouldn't" do this? if {![info exists before_pkgs]} { set before_pkgs [package names] + #update the before_dict which persists across while loop + #we need to track the actual 'ifneeded' script not just version numbers, + #because the last ifneeded script processed for each version is the one that ultimately applies. + foreach bp $before_pkgs { + #dict set before_dict $bp [package versions $bp] + foreach v [package versions $bp] { + dict set before_dict $bp $v [package ifneeded $bp $v] + } + } } - #update the before_dict which persists across while loop - foreach bp $before_pkgs { - dict set before_dict $bp [package versions $bp] - } - catch { + #set before_pkgs [package names] + + #catch { foreach file $indexfiles { set dir [file dirname $file] if {![info exists procdDirs($dir)]} { try { - #puts stderr "----->0 sourcing $file" + #if {[string match //zipfs*registry* $file]} { + # puts stderr "----->0 sourcing zipfs file $file" + #} incr sourced ;#count as sourced even if source fails; keep before actual source action #::tcl::Pkg::source $file + #lappend sourced_files $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (1)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (1)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" @@ -532,8 +607,11 @@ tcl::namespace::eval punk::libunknown { set procdDirs($dir) 1 } } + #each source operation could affect auto_path - and thus increment the pkg epoch (via trace on ::auto_path) + #e.g tcllib pkgIndex.tcl appends to auto_path + set pkg_epoch [dict get $epoch pkg current] } - } + #} set dir [lindex $use_path end] if {![info exists procdDirs($dir)]} { set file [file join $dir pkgIndex.tcl] @@ -542,20 +620,24 @@ tcl::namespace::eval punk::libunknown { try { #puts "----->2 sourcing $file" incr sourced + #lappend sourced_files $file #::tcl::Pkg::source $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (2)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (2)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" } on ok {} { set procdDirs($dir) 1 } + set pkg_epoch [dict get $epoch pkg current] } } #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath [dict create] @@ -568,34 +650,89 @@ tcl::namespace::eval punk::libunknown { } set after_pkgs [package names] - set just_added [dict create] + #puts "@@@@pkg epochs $pkg_epoch searchpath:$currentsearchpath name:$name before: [llength $before_pkgs] after: [llength $after_pkgs]" if {[llength $after_pkgs] > [llength $before_pkgs]} { foreach a $after_pkgs { - if {![dict exists $before_dict $a]} { - dict set just_added $a 1 - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $pkg_epoch + foreach v [package versions $a] { + if {![dict exists $before_dict $a $v]} { + dict set just_added $a $v 1 + set iscript [package ifneeded $a $v] + #J2 + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a [dict create e $pkg_epoch v $v] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $v [dict create e $pkg_epoch scr $iscript] + if {$must_scan} { + dict unset epoch pkg untracked $a + } + } } } - #puts stderr ">>>zipfs_tclPkgUnknown added [llength $added_pkgs]" - #puts stderr ">>> [join [lrange $added_pkgs 0 10] \n]..." } - dict for {bp bpversions} $before_dict { - if {[dict exists $just_added $bp]} { - continue - } - if {[llength $bpversions] != [llength [package versions $bp]]} { - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $pkg_epoch + + #----------------- + #if {[dict size $just_added]} { + # puts stderr "\x1b\[31m>>>zipfs_tclPkgUnknown called on name:$name added [dict size $just_added] from searchpath:$currentsearchpath\x1b\[m" + # puts stderr ">>> [join [lrange [dict keys $just_added] 0 10] \n]..." + #} else { + # tclLog ">>>zipfs_tclPkgUnknown called on name:$name Nothing added for searchpath:$currentsearchpath" + # if {[string match twapi* $name]} { + # tclLog ">>>zipfs_tclPkgUnknown: sourced_files:" + # foreach f $sourced_files { + # puts ">>> $f" + # } + # } + # if {$currentsearchpath in "//zipfs:/app //zipfs:/app/tcl_library"} { + # puts " before_pkgs: [llength $before_pkgs]" + # puts " lsearch msgcat: [lsearch $before_pkgs msgcat]" + # puts " after_pkgs: [llength $after_pkgs]" + # puts " \x1b\31mlsearch msgcat: [lsearch $after_pkgs msgcat]\x1b\[m" + # if {[lsearch $after_pkgs msgcat] >=0} { + # set versions [package versions msgcat] + # puts "msgcat versions: $versions" + # foreach v $versions { + # puts "\x1b\[32m $v ifneeded: [package ifneeded msgcat $v] \x1b\[m" + # } + # } + # } + #} + #----------------- + + #review - just because this searchpath didn't add a package or add a version for the package + #it doesn't mean there wasn't a version of this package supplied there + #It may just be the same version as one we've already found. + #The last one found (earlier in auto_path) for a version is the one that supplies the final 'package provide' statement (by overriding it) + # + dict for {bp bpversionscripts} $before_dict { + #if {!$must_scan && ![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $bp]} { + # #puts -nonewline . + # continue + #} + dict for {bv bscript} $bpversionscripts { + set nowscript [package ifneeded $bp $bv] + if {$bscript ne $nowscript} { + #ifneeded script has changed. The same version of bp was supplied on this path. + #As it's processed later - it will be the one in effect. + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp [dict create e $pkg_epoch v $bv] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $bv [dict create e $pkg_epoch scr $nowscript] + dict set before_dict $bp $bv $nowscript + dict set just_changed $bp $bv 1 + #j2 + if {$must_scan} { + dict unset epoch pkg untracked $bp + } + } } } - #puts stderr "zipfs_tclPkgUnknown $tid sourced: $sourced (under path: $currentsearchpath)" - if {$name ni $after_pkgs} { - #cache negative result (for this epoch only) - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } elseif {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } - lappend before_pkgs {*}[dict keys $just_added] + #update before_pkgs & before_dict for next path + dict for {newp vdict} $just_added { + if {$newp ni $before_pkgs} { + lappend before_pkgs $newp + } + dict for {v _} $vdict { + set nowscript [package ifneeded $newp $v] + dict set before_dict $newp $v $nowscript + } + } } } @@ -680,20 +817,143 @@ tcl::namespace::eval punk::libunknown { } } set old_path $auto_path + + dict for {pkg versions} $just_changed { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + dict for {pkg versions} $just_added { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + } + + #refresh ifneeded scripts for just_added/just_changed + #review: searchpaths are in auto_path order - earliest has precedence for any particular pkg-version + + #REVIEW: what is to stop an auto_path package e.g from os, overriding a .tm ifneeded script from an item earlier in the package_mode list configured in punk's main.tcl? + #e.g when package_mode is {dev-os} we don't want a pkgIndex package from ::env(TCLLIBPATH) overriding a .tm from the dev paths (even if version nums the same) + #conversely we do want a dev path pkIndex package overriding an existing ifneeded script from a .tm in os + #to accomodate this - we may need to maintain a subdict in epoch of paths/path-prefixes to package_mode members os, dev, internal + + #this 'refresh' is really a 'reversion' to what was already stored in epoch pkg epochs added + # + + set e [dict get $epoch pkg current] + set pkgvdone [dict create] + set dict_added [dict get $epoch pkg epochs $e added] + #keys are in reverse order due to tclPkgUnknown processing order + set ordered_searchpaths [lreverse [dict keys $dict_added]];# ordered as in auto_path + + dict for {pkg versiond} $refresh_dict { + set versions [dict keys $versiond] + puts stderr "---->pkg:$pkg versions: $versions" + foreach searchpath $ordered_searchpaths { + set addedinfo [dict get $dict_added $searchpath] + set vidx -1 + foreach v $versions { + incr vidx + if {[dict exists $addedinfo $pkg $v]} { + ledit versions $vidx $vidx + set iscript [dict get $addedinfo $pkg $v scr] + #todo - find the iscript in the '$epoch pkg epochs added paths' lists and determine os vs dev vs internal + #(scanning for path directly in the ifneeded script for pkgs is potentially error prone) + #for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway) + set justaddedscript [package ifneeded $pkg $v] + if {$justaddedscript ne $iscript} { + puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions" + package ifneeded $pkg $v $iscript + #dict set pkgvdone $pkg $v 1 + } + } + } + if {[llength $versions] == 0} { + break + } + } } + + #puts "zipfs_tclPkgUnknown DONE" } + variable last_auto_path + variable last_tm_paths proc epoch_incr_pkg {args} { if {[catch { + variable last_auto_path global auto_path upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] + # ------------- + puts stderr "--> pkg epoch $prev_e -> $current_e" + puts stderr "args: $args" + puts stderr "last_auto: $last_auto_path" + puts stderr "auto_path: $auto_path" + # ------------- + if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { + #The auto_path changed, and is a pure addition of entry/entries + #commonly this is occurs where a single entry is added by a pkgIndex.Tcl + #e.g tcllib adds its base dir so that all pkgIndex.tcl files in subdirs are subsequently found + #consider autopath + #c:/libbase //zipfs:/app/libbase + #if both contain a tcllib folder with pkgIndex.tcl that extends auto_path, the auto_path extends as follows: + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase/tcllib + + #the tclPkgUnknown usedir loop (working from end of list towards beginning) will process these changes the first time dynamically + #as they occur: + #ie //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase c:/libbase/tcllib + + #A subsequent scan by tclPkgUnknown on the extended auto_path would process in the order: + #c:/libbase/tcllib c:/libbase //zipfs:/app/libbase/tcllib //zipfs:/app/libbase + + #re-order the new additions to come immediately following the longest common prefix entry + + set newitems [punk::libunknown::lib::ldiff $auto_path $last_auto_path] + + set update $last_auto_path + #no ledit or punk::lib::compat::ledit for 8.6 - so use linsert + foreach new $newitems { + set offset 0 + set has_prefix 0 + foreach ap [lreverse $update] { + if {[string match $ap* $new]} { + set has_prefix 1 + break + } + incr offset + } + if {$has_prefix} { + set update [linsert $update end-$offset $new] + } else { + lappend update $new + } + } + set auto_path $update + + + } + #else - if auto_path change wasn't just extra entries - leave as user specified + #review. + + set last_auto_path $auto_path + # ------------- dict set epoch pkg current $current_e dict set epoch pkg epochs $current_e [dict create] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } + if {[dict exists $epoch pkg epochs $prev_e indexes]} { - #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + #bring across each previous 'indexes' record *if* searchpath is within zipfs root static filesystem + # and searchpath is still a path below an auto_path entry. + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -710,6 +970,9 @@ tcl::namespace::eval punk::libunknown { } } } + + #---------------------------------------- + #store basic stats for previous epoch instead of all data. set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e indexes]] set index_count 0 dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { @@ -718,12 +981,28 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count $index_searchpath_count index_count $index_count] dict unset epoch pkg epochs $prev_e indexes + #---------------------------------------- } else { dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch pkg epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + if {"clearadded" in $args} { + dict set epoch pkg epochs $current_e added [dict create] + } else { + if {$has_zipfs} { + set zroot [zipfs root] + set prev_added [dict get $epoch pkg epochs $prev_e added] + set keep_added [dict filter $prev_added key $zroot*] + #bring across - each lib will have previous epoch number as the value indicating epoch in which it was found + #dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + dict set epoch pkg epochs $current_e added $keep_added + } else { + dict set epoch pkg epochs $current_e added [dict create] + } + } + + #store basic stats for previous epoch + #------------------------------------ set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e added] { @@ -735,37 +1014,31 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] dict unset epoch pkg epochs $prev_e added + #------------------------------------ } else { dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch pkg epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch pkg epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch pkg epochs $prev_e unfound - } } errM]} { - puts stderr "epoch_incr_pkg error\n $errM" + puts stderr "epoch_incr_pkg error\n $errM\n$::errorInfo" } } proc epoch_incr_tm {args} { if {[catch { upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch tm current] set current_e [expr {$prev_e + 1}] dict set epoch tm current $current_e dict set epoch tm epochs $current_e [dict create] set tmlist [tcl::tm::list] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } if {[dict exists $epoch tm epochs $prev_e indexes]} { #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch tm epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -795,8 +1068,21 @@ tcl::namespace::eval punk::libunknown { dict set epoch tm epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch tm epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + #todo? cycle through non-statics and add pkgs to pkg untracked if we are deleting 'added' records? + if {"clearadded" in $args} { + dict set epoch tm epochs $current_e added [dict create] + } else { + #bring across - each lib will have previous epoch number + #dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + if {$has_zipfs} { + set zroot [zipfs root] + dict set epoch tm epochs $current_e added [dict filter [dict get $epoch tm epochs $prev_e added] key $zroot*] + } else { + dict set epoch tm epochs $current_e added [dict create] + } + } + + set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e added] { @@ -811,26 +1097,77 @@ tcl::namespace::eval punk::libunknown { } else { dict set epoch tm epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch tm epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch tm epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch tm epochs $prev_e unfound - } } errM]} { puts stderr "epoch_incr_tm error\n $errM" } } - proc init {} { + #see what basic info we can gather *quickly* about the indexes for each version of a pkg that the package db knows about. + #we want no calls out to the actual filesystem - but we can use some 'file' calls such as 'file dirname', 'file split' (review -safe interp problem) + #in practice the info is only available for tm modules + proc packagedb_indexinfo {pkg} { + if {[string match ::* $pkg]} { + error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'" + } + set versions [lsort -command {package vcompare} [package versions $pkg]] + if {[llength $versions] == 0} { + set v [package provide $pkg] + } + set versionlist [list] + foreach v $versions { + set ifneededscript [package ifneeded $pkg $v] + if {[string trim $ifneededscript] eq ""} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + set scriptlines [split $ifneededscript \n] + if {[llength $scriptlines] > 1} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[catch {llength $ifneededscript}]} { + #scripts aren't necessarily 'list shaped' - we don't want to get into the weeds trying to make sense of arbitrary scripts. + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[lindex $ifneededscript 0] eq "package" && [lindex $ifneededscript 1] eq "provide" && [file extension [lindex $ifneededscript end]] eq ".tm"} { + set tmfile [lindex $ifneededscript end] + set nspath [namespace qualifiers $pkg] + if {$nspath eq ""} { + set base [file dirname $tmfile] + } else { + set nsparts [string map {:: " "} $nspath] ;#*naive* split - we are assuming (fairly reasonably) there are no namespaces containing spaces for a .tm module + set pathparts [file split [file dirname $tmfile]] + set baseparts [lrange $pathparts 0 end-[llength $nsparts]] + set base [file join {*}$baseparts] + } + lappend versionlist [list $v type tm index $tmfile indexbase $base script $ifneededscript] + } else { + #we could guess at the pkgindex.tcl file used based on simple pkg ifneeded scripts .tcl path compared to ::auto_index + #but without hitting filesystem to verify - it's unsatisfactory + lappend versionlist [list $v type unknown index "" indexbase "" script $ifneededscript] + } + } + return $versionlist + } + proc init {args} { + variable last_auto_path + set last_auto_path [set ::auto_path] + variable last_tm_paths + set last_tm_paths [set ::tcl::tm::paths] + + set callerposn [lsearch $args -caller] + if {$callerposn > -1} { + set caller [lindex $args $callerposn+1] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - caller:$caller\x1b\[m" + #puts stderr "punk::libunknown::init auto_path : $::auto_path" + #puts stderr "punk::libunknown::init tcl::tm::list: [tcl::tm::list]" + } + + + + if {[catch {tcl::tm::list} tmlist]} { set tmlist [list] } @@ -850,10 +1187,113 @@ tcl::namespace::eval punk::libunknown { #This is far from conclusive - there may be other renamers (e.g commandstack) return } + + + if {[info commands ::punk::libunknown::package] ne ""} { puts stderr "punk::libunknown::init already done - unnecessary call? info frame -1: [info frame -1]" return } + variable epoch + if {![info exists epoch]} { + set tmstate [dict create 0 {added {}}] + set pkgstate [dict create 0 {added {}}] + set tminfo [dict create current 0 epochs $tmstate untracked [dict create]] + set pkginfo [dict create current 0 epochs $pkgstate untracked [dict create]] + + set epoch [dict create scripts {} tm $tminfo pkg $pkginfo] + + #untracked: package names at time of punk::libunknown::init call - or passed with epoch when sharing epoch to another interp. + #The epoch state will need to be incremented and cleared of any 'added' records if any of these are requested during a package unknown call + #Because they were loaded prior to us tracking the epochs - and without trying to examine the ifneeded scripts we don't know the exact paths + #which were scanned to load them. Our 'added' key entries will not contain them because they weren't unknown + } else { + #we're accepting a pre-provided 'epoch' record (probably from another interp) + #the tm untracked and pkg untracked dicts indicate for which packages the pkg added, tm added etc data are not conclusive + #test + #todo? + } + #upon first libunknown::init in the interp, we need to add any of this interp's already known packages to the (possibly existing) tm untracked and pkg untracked dicts. + #(unless we can use packagedb_indexinfo to determine what was previously scanned?) + # review - what if the auto_path or tcl::tm::list was changed between initial scan and call of libunknown::init??? + # This is likely a common scenario?!!! + # For now this is a probable flaw in the logic - we need to ensure libunknown::init is done first thing + # or suffer additional scans.. or document ?? + #ideally init should be called in each interp before any scans for packages so that the list of untracked is minimized. + set pkgnames [package names] + foreach p $pkgnames { + if {[string tolower $p] in {punk::libunknown tcl::zlib tcloo tcl::oo tcl}} { + continue + } + set versions [package versions $p] + if {[llength $versions] == 0} { + continue + } + set versionlist [packagedb_indexinfo $p] + if {[llength $versionlist] == 0} { + continue + } else { + foreach vdata $versionlist { + #dict set epoch scripts $p [lindex $vdata 0] [package ifneeded $p [lindex $vdata 0]] + dict set epoch scripts $p [lindex $vdata 0] [lindex $vdata 8]] + } + if {[lsearch -index 6 $versionlist ""] > -1} { + #There exists at least one empty indexbase for this package - we have to treat it as untracked + dict set epoch tm untracked $p "" ;#value unimportant + dict set epoch pkg untracked $p "" ;#value unimportant + } else { + #update the epoch info with where the tm versions came from + #(not tracking version numbers in epoch - just package to the indexbase) + foreach vdata $versionlist { + lassign $vdata v _t type _index index _indexbase indexbase _script iscript + if {$type eq "tm"} { + if {![dict exists $epoch tm epochs 0 added $indexbase]} { + #dict set epoch tm epochs 0 added $indexbase [dict create $p [dict create e 0 v $v]] + dict set epoch tm epochs 0 added $indexbase $p $v [dict create e 0 scr $iscript] + } else { + set idxadded [dict get $epoch tm epochs 0 added $indexbase] + #dict set idxadded $p [dict create e 0 v $v] + dict set idxadded $p $v [dict create e 0 scr $iscript] + dict set epoch tm epochs 0 added $indexbase $idxadded + } + dict unset epoch tm untracked $p + } elseif {$type eq "pkg"} { + #todo? tcl doesn't give us good introspection on package indexes for packages + #dict unset epoch pkg untracked $p + } + } + } + } + } + + + + + #------------------------------------------------------------- + #set all_untracked [dict keys [dict get $epoch untracked]] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - pkg all_untracked:\x1b\[m [dict size [dict get $epoch pkg untracked]]" + #if {[dict exists $epoch pkg untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in pkg untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in pkg untracked \x1b\[m " + #} + #puts stderr "\x1b\[1\;33m punk::libunknown::init - tm all_untracked:\x1b\[m [dict size [dict get $epoch tm untracked]]" + #if {[dict exists $epoch tm untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in tm untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in tm untracked \x1b\[m " + #} + #------------------------------------------------------------- trace add variable ::auto_path write ::punk::libunknown::epoch_incr_pkg trace add variable ::tcl::tm::paths write ::punk::libunknown::epoch_incr_tm @@ -870,6 +1310,7 @@ tcl::namespace::eval punk::libunknown { #forgetting Tcl or tcl seems to be a bad idea - package require doesn't work afterwards (independent of this pkg) set forgets_requested [lrange $args 1 end] set ok_forgets [list] + upvar ::punk::libunknown::epoch epoch foreach p $forgets_requested { #'package files' not avail in early 8.6 #There can be other custom 'package ifneeded' scripts that don't use source - but still need to be forgotten. @@ -880,7 +1321,7 @@ tcl::namespace::eval punk::libunknown { # lappend ok_forgets $p #} #What then? Hardcoded only for now? - if {$p ni {tcl Tcl tcl::oo}} { + if {$p ni {tcl Tcl tcl::oo tk}} { #tcl::oo returns a comment only for its package provide script "# Already present, OK?" # - so we can't use empty 'ifneeded' script as a determinant. set vpresent [package provide $p] @@ -890,11 +1331,13 @@ tcl::namespace::eval punk::libunknown { set ifneededscript [package ifneeded $p $vpresent] if {[string trim $ifneededscript] ne ""} { lappend ok_forgets $p + dict unset epoch scripts $p } } else { #not loaded - but may have registered ifneeded script(s) in the package database #assume ok to forget lappend ok_forgets $p + dict unset epoch scripts $p } } } @@ -1021,7 +1464,9 @@ tcl::namespace::eval punk::libunknown { #} if {![interp issafe]} { + #J2 package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown} + #package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::tclPkgUnknown} } } @@ -1030,11 +1475,280 @@ tcl::namespace::eval punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + proc package_query {pkgname} { + variable epoch + + if {[dict exists $epoch tm untracked $pkgname]} { + set pkg_info "$pkgname tm UNTRACKED" + } else { + set pkg_info "$pkgname not in tm untracked" + } + if {[dict exists $epoch pkg untracked $pkgname]} { + append pkg_info \n "$pkgname pkg UNTRACKED" + } else { + append pkg_info \n "$pkgname not in pkg untracked" + } + + set pkg_epoch [dict get $epoch pkg current] + #set epoch_info [dict get $epoch pkg epochs $pkg_epoch] + #pkg entries are processed by package unknown in reverse - so their order of creaation is opposite to ::auto_path + set r_added [dict create] + foreach path [lreverse [dict keys [dict get $epoch pkg epochs $pkg_epoch added]]] { + dict set r_added $path [dict get $epoch pkg epochs $pkg_epoch added $path] + } + + #set pkg_added [punk::lib::showdict $r_added */$pkgname] + #set added [textblock::frame -title $title $pkg_added] + set rows [list] + dict for {path pkgs} $r_added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + set scriptlen [string length [dict get $data scr]] + append c3 "$v epoch[dict get $data e] ifneededchars:$scriptlen" \n + } + } + set r [list $path $c2 $c3] + lappend rows $r + } + set title "[punk::ansi::a+ green] PKG epoch $pkg_epoch - added [punk::ansi::a]" + set added [textblock::table -title $title -headers [list Path Pkgcount $pkgname] -rows $rows] + + + set pkg_row $added + + set tm_epoch [dict get $epoch tm current] + #set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + set added [dict get $epoch tm epochs $tm_epoch added] + set rows [list] + dict for {path pkgs} $added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + append c3 "$v $data" \n + } + } + set r [list $c1 $c2 $c3] + lappend rows $r + } + set title "TM epoch $tm_epoch - added" + #set added [textblock::frame -title $title $tm_added] + set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows] + + set tm_row $added + + return $pkg_info\n$pkg_row\n$tm_row + } + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::libunknown ---}] } # == === === === === === === === === === === === === === === +namespace eval punk::libunknown { + #for 8.6 compat + if {"::ledit" ni [info commands ::ledit]} { + #maint: taken from punk::lib + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } + + #maint: taken from punk::lib + proc lindex_resolve {len index} { + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + if {[string is integer -strict $index]} { + #can match +i -i + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } else { + #integer may still have + sign - normalize with expr + return [expr {$index}] + } + } else { + if {[string match end* $index]} { + if {$index ne "end"} { + set op [string index $index 3] + set offset [string range $index 4 end] + if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} + if {$op eq "+" && $offset != 0} { + return -2 + } + } else { + #index is 'end' + set index [expr {$len-1}] + if {$index < 0} { + #special case - 'end' with empty list - treat end like a positive number out of bounds + return -2 + } else { + return $index + } + } + if {$offset == 0} { + set index [expr {$len-1}] + if {$index < 0} { + return -2 ;#special case as above + } else { + return $index + } + } else { + #by now, if op = + then offset = 0 so we only need to handle the minus case + set index [expr {($len-1) - $offset}] + } + if {$index < 0} { + return -3 + } else { + return $index + } + } else { + #plain +- already handled above. + #we are trying to avoid evaluating unbraced expr of potentially insecure origin + if {[regexp {(.*)([+-])(.*)} $index _match a op b]} { + if {[string is integer -strict $a] && [string is integer -strict $b]} { + if {$op eq "-"} { + set index [expr {$a - $b}] + } else { + set index [expr {$a + $b}] + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } + return $index + } + } + } + } +} + +tcl::namespace::eval punk::libunknown::lib { + + #A version of textutil::string::longestCommonPrefixList + #(also as ::punk::lib::longestCommonPrefixList) + proc longestCommonPrefix {items} { + if {[llength $items] <= 1} { + return [lindex $items 0] + } + set items [lsort $items[unset items]] + set min [lindex $items 0] + set max [lindex $items end] + #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) + #(sort order nothing to do with length - e.g min may be longer than max) + if {[string length $min] > [string length $max]} { + set temp $min + set min $max + set max $temp + } + set n [string length $min] + set prefix "" + set i -1 + while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { + append prefix $c + } + return $prefix + } + #maint: from punk::lib::ldiff + proc ldiff {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + set result [list] + foreach item $fromlist { + if {$item ni $removeitems} { + lappend result $item + } + } + return $result + } + proc intersect2 {A B} { + #taken from tcl version of struct::set::Intersect + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + + # This is slower than local vars, but more robust + if {[llength $B] > [llength $A]} { + ::set res $A + ::set A $B + ::set B $res + } + ::set res {} + foreach x $A { + ::set ($x) {} + } + foreach x $B { + if {[info exists ($x)]} { + lappend res $x + } + } + return $res + } + proc is_list_all_in_list {A B} { + if {[llength $A] > [llength $B]} {return 0} + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } +} # ----------------------------------------------------------------------------- # register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix-0.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix-0.2.tm index 24ef156c..1ac6a836 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix-0.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix-0.2.tm @@ -9,12 +9,12 @@ tcl::namespace::eval punk::mix { package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap set t [time { - if {[catch {punk::mix::templates::provider register *} errM]} { - puts stderr "punk::mix failure during punk::mix::templates::provider register *" - puts stderr $errM - puts stderr "-----" - puts stderr $::errorInfo - } + if {[catch {punk::mix::templates::provider register *} errM]} { + puts stderr "punk::mix failure during punk::mix::templates::provider register *" + puts stderr $errM + puts stderr "-----" + puts stderr $::errorInfo + } }] puts stderr "->punk::mix::templates::provider register * t=$t" } diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm index 5e12b9a2..3fb1e001 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm @@ -53,11 +53,6 @@ namespace eval punk::mix::commandset::loadedlib { #REVIEW - this doesn't result in full scans catch {package require frobznodule666} ;#ensure pkg system has loaded/searched for everything - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } set packages [package names] set matches [list] foreach search $searchstrings { @@ -85,11 +80,7 @@ namespace eval punk::mix::commandset::loadedlib { # set versions $v #} } - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {$opt_highlight} { set v [package provide $m] if {$v ne ""} { @@ -188,11 +179,6 @@ namespace eval punk::mix::commandset::loadedlib { } proc info {libname} { - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } catch {package require $libname 1-0} ;#ensure pkg system has loaded/searched - using unsatisfiable version range set pkgsknown [package names] if {[set posn [lsearch $pkgsknown $libname]] >= 0} { @@ -201,11 +187,7 @@ namespace eval punk::mix::commandset::loadedlib { puts stderr "Package not found as available library/module - check tcl::tm::list and \$auto_path" } set versions [package versions [lindex $libname 0]] - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {![llength $versions]} { puts stderr "No version numbers found for library/module $libname" return false diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 2bc0f01c..723ce06e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -77,6 +77,12 @@ namespace eval punk::mix::commandset::module { return $result } #require current dir when calling to be the projectdir, or + punk::args::define { + @dynamic + @id -id "::punk::mix::commandset::module::templates" + @cmd -name "punk::mix::commandset::module::templates" + ${[punk::args::resolved_def -antiglobs {@id @cmd} "::punk::mix::commandset::module::templates_dict"]} + } proc templates {args} { set tdict_low_to_high [templates_dict {*}$args] #convert to screen order - with higher priority at the top @@ -135,16 +141,17 @@ namespace eval punk::mix::commandset::module { globsearches -default * -multiple 1 } proc templates_dict {args} { - set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + #set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + set argd [punk::args::parse $args withid ::punk::mix::commandset::module::templates_dict] package require punk::cap if {[punk::cap::capability_has_handler punk.templates]} { set template_folder_dict [punk::cap::call_handler punk.templates get_itemdict_moduletemplates {*}$args] } else { put stderr "get_template_basefolders WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } - } + } + - set moduletypes [punk::mix::cli::lib::module_types] punk::args::define [subst { @id -id ::punk::mix::commandset::module::new @@ -178,7 +185,7 @@ namespace eval punk::mix::commandset::module { set argd [punk::args::get_by_id ::punk::mix::commandset::module::new $args] lassign [dict values $argd] leaders opts values received set module [dict get $values module] - + #set opts [dict merge $defaults $args] #todo - review compatibility between -template and -type diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index f670c8c0..8abe694e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -592,10 +592,23 @@ namespace eval punk::mix::commandset::project { namespace export * namespace path [namespace parent] + punk::args::define { + @id -id ::punk::mix::commandset::project::collection::_default + @cmd -name "punk::mix::commandset::project::collection::_default"\ + -summary\ + "List projects under fossil managment."\ + -help\ + "List projects under fossil management, showing fossil db location and number of checkouts" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * + } #e.g imported as 'projects' - proc _default {{glob {}} args} { + proc _default {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::collection::_default] + set globlist [dict get $argd values glob] + #*** !doctools - #[call [fun _default] [arg glob] [opt {option value...}]] + #[call [fun _default] [arg glob...]] #[para]List projects under fossil management, showing fossil db location and number of checkouts #[para]The glob argument is optional unless option/value pairs are also supplied, in which case * should be explicitly supplied #[para]glob restricts output based on the name of the fossil db file e.g s* for all projects beginning with s @@ -604,7 +617,7 @@ namespace eval punk::mix::commandset::project { #[para] punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection #[para]Will result in the command being available as projects package require overtype - set db_projects [lib::get_projects $glob] + set db_projects [lib::get_projects {*}$globlist] set col1items [lsearch -all -inline -index 0 -subindices $db_projects *] set col2items [lsearch -all -inline -index 1 -subindices $db_projects *] set checkouts [lsearch -all -inline -index 2 -subindices $db_projects *] @@ -1012,12 +1025,21 @@ namespace eval punk::mix::commandset::project { #consider using punk::cap to enable multiple template-substitution providers with their own set of tagnames and/or tag wrappers, where substitution providers are all run return [string cat % $tagname %] } - #get project info only by opening the central confg-db - #(will not have proper project-name etc) - proc get_projects {{globlist {}} args} { - if {![llength $globlist]} { - set globlist [list *] - } + punk::args::define { + @id -id ::punk::mix::commandset::project::lib::get_projects + @cmd -name punk::mix::commandset::project::lib::get_projects\ + -summary\ + "List projects referred to by central fossil config-db."\ + -help\ + "Get project info only by opening the central fossil config-db + (will not have proper project-name etc)" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * -optional 1 + } + proc get_projects {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::lib::get_projects] + set globlist [dict get $argd values glob] + set fossil_prog [auto_execok fossil] set configdb [punk::repo::fossil_get_configdb] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index e2f44ad3..b40be865 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -940,7 +940,8 @@ tcl::namespace::eval punk::nav::fs { #windows doesn't consider dotfiles as hidden - mac does (?) #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden if {$::tcl_platform(platform) ne "windows"} { - lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + #lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + lappend flaggedhidden {*}[tcl::prefix::all [list {*}$dirs {*}$files] .] #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely #set flaggedhidden [lsort -unique $flaggedhidden] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 0f609b4f..6bd826e2 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -177,10 +177,10 @@ tcl::namespace::eval punk::ns { } else { set fq_nspath $nspath } - if {[catch {nseval_ifexists $fq_nspath {}}]} { - return 0 - } else { + if {[nseval_ifexists $fq_nspath {::string cat ok}] eq "ok"} { return 1 + } else { + return 0 } } @@ -408,6 +408,7 @@ tcl::namespace::eval punk::ns { proc nstail {nspath args} { #normalize the common case of :::: set nspath [string map {:::: ::} $nspath] + #it's unusual - but namespaces *can* have spaced in them. set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] @@ -757,13 +758,20 @@ tcl::namespace::eval punk::ns { } set marks [dict create oo \u25c6 ooc \u25c7 ooo \u25c8 punkargs \U1f6c8 ensemble \u24ba native \u24c3 unknown \U2370] if {[llength $ansinames]} { - return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type][punk::ansi::a]" + return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type]\x1b\[0m" } else { return [dict get $marks $type] } } #REVIEW - ansi codes can be *very* confusing to the user when trying to handle lists etc.. + punk::args::define { + @id -id ::punk::ns::get_nslist + @cmd -name punk::ns::get_nslist + @opts + -match -default "" + -nsdict -type dict -default {} + } proc get_nslist {args} { set known_types [list children commands exported imported aliases procs ensembles ooclasses ooobjects ooprivateobjects ooprivateclasses native coroutines interps zlibstreams] set defaults [dict create\ @@ -774,6 +782,9 @@ tcl::namespace::eval punk::ns { set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- set fq_glob [dict get $opts -match] + if {$fq_glob eq ""} { + set fq_glob [uplevel 1 nsthis]::* + } set requested_types [dict get $opts -types] set opt_nsdict [dict get $opts -nsdict] @@ -834,7 +845,7 @@ tcl::namespace::eval punk::ns { set zlibstreams [list] set usageinfo [list] - if {$opt_nsdict eq ""} { + if {![dict size $opt_nsdict]} { set nsmatches [get_ns_dicts $fq_glob -allbelow 0] set itemcount 0 set matches_with_results [list] @@ -866,6 +877,8 @@ tcl::namespace::eval punk::ns { } if {"commands" in $types} { set commands [dict get $contents commands] + } + set usageinfo [dict get $contents usageinfo] foreach t $types { switch -- $t { exported { @@ -909,8 +922,6 @@ tcl::namespace::eval punk::ns { } } } - set usageinfo [dict get $contents usageinfo] - } set numchildren [llength $children] if {$numchildren} { @@ -1067,7 +1078,7 @@ tcl::namespace::eval punk::ns { } else { } if {$cmd in $imported} { - set prefix [overtype::right $prefix "-[a+ yellow bold]I[a+]"] + set prefix [overtype::right $prefix "-[a+ yellow bold]I[a]"] } } if {$cmd in $usageinfo} { @@ -1075,7 +1086,8 @@ tcl::namespace::eval punk::ns { } else { set u "" } - set cmd$i "${prefix} $c$cmd_display$u" + #set cmd$i "${prefix} $c$cmd_display$u" + set cmd$i "${prefix} [punk::ansi::ansiwrap -rawansi $c $cmd_display]$u" #set c$i $c set c$i "" lappend seencmds $cmd @@ -1146,7 +1158,11 @@ tcl::namespace::eval punk::ns { the child namespaces and commands within the namespace(s) matched by glob." @opts - -nspathcommands -type boolean -default 0 + -nspathcommands -type boolean -default 0 -help\ + "When a namespace has entries configured in 'namespace path', the default result for nslist + will display just a basic note: 'Also resolving cmds in namespace paths: '. + If -nspathcommands is true, it will also display subtables showing the commands resolvable + via any such listed namespaces." -types @values -min 0 -max -1 glob -multiple 1 -optional 1 -default "*" @@ -1205,9 +1221,9 @@ tcl::namespace::eval punk::ns { if {[dict size [dict get $nsdict namespacepath]]} { set path_text "" if {!$opt_nspathcommands} { - append path_text \n " also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" + append path_text \n " Also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" } else { - append path_text \n " also resolving cmds in namespace paths:" + append path_text \n " Also resolving cmds in namespace paths:" set nspathdict [dict get $nsdict namespacepath] if {!$has_textblock} { dict for {k v} $nspathdict { @@ -1216,8 +1232,14 @@ tcl::namespace::eval punk::ns { append path_text \n " cmds: $cmds" } } else { + #todo - change to display in column order to be same as main command listing dict for {k v} $nspathdict { - set t [textblock::list_as_table -title $k -columns 6 [lsort [dict get $v commands]]] + set pathcommands [dict get $v commands] + set columns 6 + if {[llength $pathcommands] < 6} { + set columns [llength $v] + } + set t [textblock::list_as_table -title $k -columns $columns [lsort $pathcommands]] append path_text \n $t } } @@ -1423,7 +1445,7 @@ tcl::namespace::eval punk::ns { } } return $matches - }] + }]] } else { lappend matched {*}[tcl::namespace::eval $location [list ::info commands [nsjoin ${location} $p]]] @@ -2397,14 +2419,16 @@ tcl::namespace::eval punk::ns { if {$is_ensembleparam} { #review lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } if {![llength $allchoices]} { #review - only leaders with a defined set of choices are eligible for consideration as a subcommand lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } @@ -2420,7 +2444,8 @@ tcl::namespace::eval punk::ns { } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args set resolvelist [list {*}$specid {*}$nextqueryargs {*}$queryargs_untested] @@ -2510,8 +2535,12 @@ tcl::namespace::eval punk::ns { punk::args::define { @id -id ::punk::ns::forms - @cmd -name punk::ns::forms -help\ - "Return names for each form of a command" + @cmd -name punk::ns::forms\ + -summary\ + "List command forms."\ + -help\ + "Return names for each form of a command. + Most commands are single-form and will only return the name '_default'." @opts @values -min 1 -max -1 cmditem -multiple 1 -optional 0 @@ -2523,12 +2552,37 @@ tcl::namespace::eval punk::ns { set id [dict get $cmdinfo origin] ::punk::args::forms $id } + + + punk::args::define { + @id -id ::punk::ns::eg + @cmd -name punk::ns::eg\ + -summary\ + "Return command examples."\ + -help\ + "Return the -help info from the @examples directive + in a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + } + proc eg {args} { + set argd [::punk::args::parse $args withid ::punk::ns::eg] + set cmdmembers [dict get $argd values cmditem] + set cmdinfo [uplevel 1 [list ::punk::ns::resolve_command {*}$cmdmembers]] ;#resolve from calling context + set resolved_id [dict get $cmdinfo origin] + set result [::punk::args::eg $resolved_id] + } + + punk::args::define { @id -id ::punk::ns::synopsis - @cmd -name punk::ns::synopsis -help\ + @cmd -name punk::ns::synopsis\ + -summary\ + "Return command synopsis."\ + -help\ "Return synopsis for each form of a command on separate lines. - If -form is given, supply only + If -form formname| is given, supply only the synopsis for that form. " @opts @@ -2564,8 +2618,12 @@ tcl::namespace::eval punk::ns { full - summary { set resultstr "" foreach synline [split $syn \n] { - #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n - append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + if {[string range $synline 0 1] eq "# "} { + append resultstr $synline \n + } else { + #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n + append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + } } set resultstr [string trimright $resultstr \n] #set resultstr [join [lreplace $syn 0 0 {*}$idparts] " "] @@ -2591,7 +2649,10 @@ tcl::namespace::eval punk::ns { punk::args::define { @dynamic @id -id ::punk::ns::arginfo - @cmd -name punk::ns::arginfo -help\ + @cmd -name punk::ns::arginfo\ + -summary\ + "Command usage/help."\ + -help\ "Show usage info for a command. It supports the following: 1) Procedures or builtins for which a punk::args definition has @@ -2618,6 +2679,9 @@ tcl::namespace::eval punk::ns { } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { -form -default 0 -help\ "Ordinal index or name of command form" + -grepstr -default "" -type list -typesynopsis regex -help\ + "list consisting of regex, optionally followed by ANSI names for highlighting + (incomplete - todo)" -- -type none -help\ "End of options marker Use this if the command to view begins with a -" @@ -2642,6 +2706,8 @@ tcl::namespace::eval punk::ns { set querycommand [dict get $values commandpath] set queryargs [dict get $values subcommand] + set grepstr [dict get $opts -grepstr] + set opts [dict remove $opts -grepstr] #puts stdout "---------------------arginfo: '$args' querycommand:'$querycommand' queryargs:'$queryargs'" #todo - similar to corp? review corp resolution process @@ -2905,7 +2971,8 @@ tcl::namespace::eval punk::ns { break } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args #set numvals [expr {[llength $queryargs]+1}] @@ -3020,8 +3087,11 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} new" - @cmd -name "${$origin} new" -help\ - "create object with specified command name. + @cmd -name "${$origin} new"\ + -summary\ + "Create new object instance."\ + -help\ + "create object with autogenerated command name. Arguments are passed to the constructor." @values }] @@ -3071,7 +3141,10 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} create" - @cmd -name "${$origin} create" -help\ + @cmd -name "${$origin} create"\ + -summary\ + "Create new object instance with specified command name."\ + -help\ "create object with specified command name. Arguments following objectName are passed to the constructor." @values -min 1 @@ -3124,7 +3197,10 @@ tcl::namespace::eval punk::ns { # but we may want notes about a specific destructor set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} destroy" - @cmd -name "destroy" -help\ + @cmd -name "destroy"\ + -summary\ + "delete object instance."\ + -help\ "delete object, calling destructor if any. destroy accepts no arguments." @values -min 0 -max 0 @@ -3601,6 +3677,13 @@ tcl::namespace::eval punk::ns { set msg "Undocumented command $origin. Type: $cmdtype" } } + if {[llength $grepstr] != 0} { + if {[llength $grepstr] == 1} { + return [punk::grepstr -no-linenumbers -highlight red [lindex $grepstr 0] $msg] + } else { + return [punk::grepstr -no-linenumbers -highlight [lrange $grepstr 1 end] [lindex $grepstr 0] $msg] + } + } return $msg } @@ -3620,6 +3703,21 @@ tcl::namespace::eval punk::ns { comment inserted to display information such as the namespace origin. Such a comment begins with #corp#." @opts + -syntax -default basic -choices {none basic}\ + -choicelabels { + none\ + " Plain text output" + basic\ + " Comment and bracket highlights. + This is a basic colourizer - not + a full Tcl syntax highlighter." + }\ + -help\ + "Type of syntax highlighting on result. + Note that -syntax none will always return a proper Tcl + List: proc + - but a syntax highlighter may return a string that + is not a Tcl list." @values -min 1 -max -1 commandname -help\ "May be either the fully qualified path for the command, @@ -3628,7 +3726,8 @@ tcl::namespace::eval punk::ns { } proc corp {args} { set argd [punk::args::parse $args withid ::punk::ns::corp] - set path [dict get $argd values commandname] + set path [dict get $argd values commandname] + set syntax [dict get $argd opts -syntax] #thanks to Richard Suchenwirth for the original - wiki.tcl-lang.org/page/corp #Note: modified here to support aliases and relative/absolute name (with respect to namespace .ie ::name vs name) if {[info exists punk::console::tabwidth]} { @@ -3713,7 +3812,19 @@ tcl::namespace::eval punk::ns { lappend argl $a } #list proc [nsjoin ${targetns} $name] $argl $body - list proc $resolved $argl $body + switch -- $syntax { + basic { + #rudimentary colourising only + set argl [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $argl] + set body [punk::grepstr -return all -highlight green {^\s*#.*} $body] ;#Note, will not highlight comments at end of line - like this one. + set body [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $body] + set body [punk::grepstr -return all -highlight tk-orange {\[|\]} $body] + #ansi colourised items in list format may not always have desired string representation (list escaping can occur) + #return as a string - which may not be a proper Tcl list! + return "proc $resolved {$argl} {\n$body\n}" + } + } + list proc $resolved $argl $body } @@ -3799,13 +3910,53 @@ tcl::namespace::eval punk::ns { } + punk::args::define { + @id -id ::punk::ns::pkguse + @cmd -name punk::ns::pkguse -help\ + "Load package and move to namespace of the same name if run + interactively with only pkg/namespace argument. + if script and args are supplied, the + script runs in the namespace with the args passed to the script. + + todo - further documentation" + @leaders -min 1 -max 1 + pkg_or_existing_ns -type string + @opts + -vars -type none -help\ + "whether to capture namespace vars for use in the supplied script" + -nowarnings -type none + @values -min 0 -max -1 + script -type string -optional 1 + arg -type any -optional 1 -multiple 1 + } #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically - proc pkguse {pkg_or_existing_ns args} { - lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs - set use_vars [expr {"-vars" in $runopts}] - set no_warnings [expr {"-nowarnings" in $runopts}] + proc pkguse {args} { + set argd [punk::args::parse $args withid ::punk::ns::pkguse] + lassign [dict values $argd] leaders opts values received + puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + + set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] + if {[dict exists $received script]} { + set scriptblock [dict get $values script] + } else { + set scriptblock "" + } + if {[dict exists $received arg]} { + set arglist [dict get $values arg] + } else { + set arglist [list] + } + + set use_vars [dict exists $received "-vars"] + set no_warnings [dict exists $received "-nowarnings"] + + #lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs + #set use_vars [expr {"-vars" in $runopts}] + #set no_warnings [expr {"-nowarnings" in $runopts}] + + set ver "" @@ -3883,7 +4034,7 @@ tcl::namespace::eval punk::ns { } } if {[tcl::namespace::exists $ns]} { - if {[llength $cmdargs]} { + if {[dict exists $received script]} { set binding {} #if {[info level] == 1} { # #up 1 is global @@ -3923,7 +4074,7 @@ tcl::namespace::eval punk::ns { } ] - set arglist [lassign $cmdargs scriptblock] + #set arglist [lassign $cmdargs scriptblock] if {[string first "\n" $scriptblock] <0 && [string first {$args} $scriptblock] <0} { #one liner without use of $args append scriptblock { {*}$args} diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index 317fc9de..dabf7f8e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,9 +110,9 @@ tcl::namespace::eval punk::packagepreference { #[list_begin definitions] lappend PUNKARGS [list { - @id -id ::punk::packagepreference::install - @cmd -name ::punk::packagepreference::install -help\ - "Install override for ::package builtin - for 'require' subcommand only." + @id -id ::punk::packagepreference::uninstall + @cmd -name ::punk::packagepreference::uninstall -help\ + "Uninstall override for ::package builtin - for 'require' subcommand only." @values -min 0 -max 0 }] proc uninstall {} { @@ -194,7 +194,7 @@ tcl::namespace::eval punk::packagepreference { if {!$is_exact && [llength $vwant] <= 1 } { #required version unspecified - or specified singularly set available_versions [$COMMANDSTACKNEXT_ORIGINAL versions $pkg] - if {[llength $available_versions] > 1} { + if {[llength $available_versions] >= 1} { # --------------------------------------------------------------- #An attempt to detect dll/so loaded and try to load same version #dll/so files are often named with version numbers that don't contain dots or a version number at all @@ -202,9 +202,11 @@ tcl::namespace::eval punk::packagepreference { set pkgloadedinfo [lsearch -nocase -inline -index 1 [info loaded] $pkg] if {[llength $pkgloadedinfo]} { - puts stderr "--> pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and multiple versions available" - lassign $pkgloadedinfo path name - set lcpath [string tolower $path] + if {[llength $available_versions] > 1} { + puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + } + lassign $pkgloadedinfo loaded_path name + set lc_loadedpath [string tolower $loaded_path] #first attempt to find a match for our loaded sharedlib path in a *simple* package ifneeded statement. set lcpath_to_version [dict create] foreach av $available_versions { @@ -212,17 +214,19 @@ tcl::namespace::eval punk::packagepreference { #ifneeded script not always a valid tcl list if {![catch {llength $scr} scrlen]} { if {$scrlen == 3 && [lindex $scr 0] eq "load" && [string match -nocase [lindex $scr 2] $pkg]} { + #a basic 'load ' statement dict set lcpath_to_version [string tolower [lindex $scr 1]] $av } } } - if {[dict exists $lcpath_to_version $lcpath]} { - set lversion [dict get $lcpath_to_version $lcpath] + if {[dict exists $lcpath_to_version $lc_loadedpath]} { + set lversion [dict get $lcpath_to_version $lc_loadedpath] } else { #fallback to a best effort guess based on the path - set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $path $pkg] + set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $loaded_path $pkg] } + #puts "====lcpath_to_version: $lcpath_to_version" if {$lversion ne ""} { #name matches pkg #hack for known dll version mismatch @@ -232,24 +236,103 @@ tcl::namespace::eval punk::packagepreference { if {[llength $vwant] == 1} { #todo - still check vsatisfies - report a conflict? review } - return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + #return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + try { + set result [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + } trap {} {emsg eopts} { + #REVIEW - this occurred in punkmagic (rebuild of tclmagic) - probably due to multiple versions of registry + #under different auto_path folders - and mal-ordering in punk::libunknown's tclPkgUnknown + #May be obsolete.. issue still not clear + + #A hack for 'couldn't open "": permission denied' + #This happens for example with the tcl9registry13.dll when loading from zipfs - but not in all systems, and not for all dlls. + #exact cause unknown. + #e.g + #%package ifneeded registry 1.3.7 + #- load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #%load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #couldn't open "C:/Users/sleek/AppData/Local/Temp/TCL00003cf8/tcl9registry13.dll": permission denied + + #a subsequent load of the path used in the error message works. + + #if {[string match "couldn't open \"*\": permission denied" $emsg]} {} + if {[regexp {couldn't open "(.*)":.*permission denied.*} $emsg _ newpath]} { + #Since this is a hack that shouldn't be required - be noisy about it. + puts stderr ">>> $emsg" + puts stderr "punk::packagepreference::require hack: Re-trying load of $pkg with path: $newpath" + return [load $newpath $pkg] + } else { + #puts stderr "??? $emsg" + #dunno - re-raise + return -options $eopts $emsg + } + } + return $result } + #else puts stderr "> no version determined for pkg: $pkg loaded_path: $loaded_path" } } } # --------------------------------------------------------------- - set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + #?? + #set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + + if {[regexp {[A-Z]} $pkg]} { + #legacy package names #only apply catch & retry if there was a cap - otherwise we'll double try for errors unrelated to capitalisation if {[catch {$COMMANDSTACKNEXT require [string tolower $pkg] {*}$vwant} v]} { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } } else { - return $v + set require_result $v } } else { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + #return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } + } + #--------------------------------------------------------------- + #load relevant punk::args:: package(s) + #todo - review whether 'packagepreference' is the right place for this. + #It is conceptually different from the main functions of packagepreference, + #but we don't really want to have a chain of 'package' overrides slowing performance. + #there may be a more generic way to add soft side-dependencies that the original package doesn't/can't specify. + #--------------------------------------------------------------- + + set lc_pkg [string tolower $pkg] + #todo - lookup list of docpkgs for a package? from where? + #we should have the option to not load punk::args:: at all for many(most?) cases where they're unneeded. + #e.g skip if not ::tcl_interactive? + switch -exact -- $lc_pkg { + tcl { + set docpkgs [list tclcore] + } + tk { + set docpkgs [list tkcore] + } + default { + set docpkgs [list $lc_pkg] + } + } + foreach dp $docpkgs { + #review - versions? + #we should be able to load more specific punk::args pkg based on result of [package present $pkg] + catch { + #$COMMANDSTACKNEXT require $pkg {*}$vwant + #j2 + $COMMANDSTACKNEXT require punk::args::$dp + } } + #--------------------------------------------------------------- + return $require_result } default { return [$COMMANDSTACKNEXT {*}$args] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm index ff48fcb0..54ee4080 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/path-0.1.0.tm @@ -325,7 +325,8 @@ namespace eval punk::path { lappend finalparts .. } default { - lpop finalparts + #lpop finalparts + ledit finalparts end end } } } @@ -345,7 +346,8 @@ namespace eval punk::path { switch -exact -- $p { . - "" {} .. { - lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + #lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + ledit finalparts end end ;#uses punk::lib::compat::ledit if on < 8.7 } default { lappend finalparts $p diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/pipe-1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/pipe-1.0.tm index 0b5501ac..2b0500b8 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/pipe-1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/pipe-1.0.tm @@ -373,6 +373,7 @@ tcl::namespace::eval punk::pipe::lib { if {$end_var_posn > 0} { #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. #lassign [scan $token %${end_var_posn}s%s] var spec + #lassign [punk::lib::string_splitbefore $token $end_var_posn] var spec set var [string range $token 0 $end_var_posn-1] set spec [string range $token $end_var_posn end] ;#key section includes the terminal char which ended the var and starts the spec } else { @@ -430,7 +431,7 @@ tcl::namespace::eval punk::pipe::lib { } #if {[string length $token]} { - # #lappend varlist [splitstrposn $token $end_var_posn] + # #lappend varlist [punk::lib::string_splitbefore $token $end_var_posn] # set var $token # set spec "" # if {$end_var_posn > 0} { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm index 7bf8306e..b060ab4d 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm @@ -116,7 +116,7 @@ tcl::namespace::eval punk::repl::codethread { #review/test catch {package require punk::ns} - catch {package rquire punk::repl} + catch {package require punk::repl} #variable xyz diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm index 96350c0b..97bbe591 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -420,7 +420,11 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::Addentry - @cmd -name punk::zip::Addentry -help "Add a single file at 'path' to open channel 'zipchan' + @cmd -name punk::zip::Addentry\ + -summary\ + "Add zip-entry for file at 'path'"\ + -help\ + "Add a single file at 'path' to open channel 'zipchan' return a central directory file record" @opts -comment -default "" -help "An optional comment specific to the added file" @@ -543,7 +547,7 @@ tcl::namespace::eval punk::zip { puts -nonewline $zipchan $ddesc } } - + #PK\x01\x02 Cdentral directory file header #set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3 set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems) @@ -565,7 +569,10 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::mkzip @cmd -name punk::zip::mkzip\ - -help "Create a zip archive in 'filename'" + -summary\ + "Create a zip archive in 'filename'."\ + -help\ + "Create a zip archive in 'filename'" @opts -offsettype -default "archive" -choices {archive file}\ -help "zip offsets stored relative to start of entire file or relative to start of zip-archive diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index a4113c45..50bcc2f8 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -243,14 +243,10 @@ namespace eval punkcheck { } method get_targets_exist {} { set punkcheck_folder [file dirname [$o_installer get_checkfile]] + #puts stdout "### punkcheck glob -dir $punkcheck_folder -tails {*}$o_targets" + #targets can be paths such as punk/mix/commandset/module-0.1.0.tm - glob can search levels below supplied -dir set existing [glob -nocomplain -dir $punkcheck_folder -tails {*}$o_targets] - #set existing [list] - #foreach t $o_targets { - # if {[file exists [file join $punkcheck_folder $t]]} { - # lappend existing $t - # } - #} return $existing } method end {} { diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.tm new file mode 100644 index 00000000..61120a63 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/shellfilter-0.2.tm @@ -0,0 +1,3329 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + variable is_enabled 0 + + proc disable {} { + variable is_enabled + set is_enabled 0 + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} + } + + proc enable {} { + variable is_enabled + set is_enabled 1 + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc ::shellfilter::log::open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc ::shellfilter::log::write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc ::shellfilter::log::write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc ::shellfilter::log::close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } + if {[catch {package require shellthread}]} { + shellfilter::log::disable + } else { + shellfilter::log::enable + } + +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + #method flush {ch} { + # return "" + #} + method flush {transform_handle} { + #puts stdout "" + #review - just clear o_encbuf and emit nothing? + #we wouldn't have a value there if it was convertable from the channel encoding? + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #test with set x [string repeat " \U1f6c8" 2043] + #or + #test with set x [string repeat " \U1f6c8" 683] + #most windows terminals (at least) may emit two unrecognised chars "??" at the end + + #Our goal with the while loop here is to avoid encoding conversion errors + #the source of the bogus chars in terminals is unclear. + #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. + + #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + foreach v $o_datavars { + append $v $stringdata + } + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_encbuf + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {transform_handle bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $stringdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write flush finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set o_encbuf "" + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + #set logdata [encoding convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return + } + } + + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $stringdata + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit + #todo kitty graphics \x1b_G... + #todo iterm graphics + + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_encbuf ;#buffering for partial encoding bytes + variable o_colour + variable o_do_colour + variable o_do_colourlist + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered ;#buffering for partial ansi codes + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence + #while some terminals may handle these extended attributes even when merged - we need to cater for those that + #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without + #affecting the interpretation of the other codes. + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_colourlist {} + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_encbuf "" + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + + + #todo - track when in sixel,iterm,kitty graphics data - can be very large + method Trackcodes {chunk} { + #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) + #e.g [a+ reset reset] (0;0m vs 0;m) + + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # append emit $o_do_colour$pt$o_do_normal + # #append emit $pt + #} else { + # append emit $pt + #} + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$trailing_pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$trailing_pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$trailing_pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + # append emit $o_do_colour$trailing_pt$o_do_normal + #} else { + # append emit $trailing_pt + #} + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #REVIEW - this holding a buffer without emitting as we go is ugly. + # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. + # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. + + + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { + #string index in first part of && clause to avoid some unneeded scans of whole string for this test + #we can't use 'string last' - as we need to know only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal + #set emit [string range $buf 0 end-1] + set buf "" + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + set buf "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } else { + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + set o_buffered "" + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } + } + } + if {$emit_anyway} { + #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. + + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # set emit $o_do_colour$buf$o_do_normal + #} else { + # set emit $buf + #} + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set inputbytes $o_buffered$o_encbuf + set emit [tcl::encoding::convertto $o_enc $inputbytes] + set o_buffered "" + set o_encbuf "" + return $emit + } + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + set streaminfo [my Trackcodes $stringdata] + set emit [dict get $streaminfo emit] + + #review - wrapping already done in Trackcodes + #if {[dict get $streaminfo stacksize] == 0} { + # #no ansi on the stack - we can wrap + # #review + # set outstring "$o_do_colour$emit$o_do_normal" + #} else { + #} + #if {[llength $o_codestack]} { + # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit + #} else { + # set outstring $emit + #} + #set outstring $emit + + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $emit] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + namespace export {[a-z]*} + namespace ensemble create + #todo - implement as oo ? + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tabletitle "$pipecount pipelines active" + set t [textblock::class::table new $tabletitle] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + #JMNJMN 2025 review! + #show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + + #JMNJMN + #show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + if {[catch { + ::shellfilter::log::open $tag {-syslog ""} + } err]} { + #e.g safebase interp can't load required modules such as shellthread (or Thread) + puts stderr "shellfilter::show_pipeline cannot open log" + return + } + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if {$experiment} { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {[llength $args] % 2} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.2 +}] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm index 31995bfe..d9858980 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -137,11 +137,31 @@ tcl::namespace::eval textblock { return " -choices \{$choices\} -help {algorithm choice $choicemsg} " } } + namespace eval argdoc { + tcl::namespace::import ::punk::ansi::a+ + # -- --- --- --- --- + #non colour SGR codes + # we can use these directly via ${$I} etc without marking a definition with @dynamic + #This is because they don't need to change when colour switched on and off. + set I [a+ italic] + set NI [a+ noitalic] + set B [a+ bold] + set N [a+ normal] + # -- --- --- --- --- + proc example {str} { + set str [string trimleft $str \n] + set block [punk::ansi::ansiwrap Web-gray [textblock::frame -ansibase [a+ Web-gray bold white] -ansiborder [a+ black White] -boxlimits {hl} -type block $str]] + set result [textblock::bookend_lines $block [a] "[a defaultbg] [a]"] + #puts $result + return $result + } + } + # hash_algorithm -optional 1 -choices {${[::textblock::argdoc::hash_algorithm_choices]}} -help\ # "algorithm choice" namespace eval argdoc { - set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {$[::textblock::argdoc::hash_algorithm_choices_and_help]} + set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {${[::textblock::argdoc::hash_algorithm_choices_and_help]}} punk::args::define { @dynamic @id -id ::textblock::use_hash @@ -154,7 +174,6 @@ tcl::namespace::eval textblock { } } proc use_hash {args} { - #set argd [punk::args::get_by_id ::textblock::use_hash $args] set argd [punk::args::parse $args withid ::textblock::use_hash] variable use_hash if {![dict exists $argd received hash_algorithm]} { @@ -2294,7 +2313,8 @@ tcl::namespace::eval textblock { #JMN #spanned_parts are all built with textblock::frame - therefore uniform-width lines - can use join_basic - set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + #set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + set spanned_frame [textblock::join_basic_raw {*}$spanned_parts] if {$spans_to_rhs} { if {$cidx == 0} { @@ -2363,7 +2383,8 @@ tcl::namespace::eval textblock { } else { #this_span == 1 - set spanned_frame [textblock::join_basic -- $header_cell_startspan] + #set spanned_frame [textblock::join_basic -- $header_cell_startspan] + set spanned_frame [textblock::join_basic_raw $header_cell_startspan] } @@ -3992,7 +4013,8 @@ tcl::namespace::eval textblock { set body_build "" } else { #body blocks should not be ragged - so can use join_basic - set body_build [textblock::join_basic -- {*}$body_blocks] + #set body_build [textblock::join_basic -- {*}$body_blocks] + set body_build [textblock::join_basic_raw {*}$body_blocks] } if {$headerheight > 0} { set table [tcl::string::cat $header_build \n $body_build] @@ -4149,7 +4171,6 @@ tcl::namespace::eval textblock { proc periodic {args} { #For an impressive interactive terminal app (javascript) # see: https://github.com/spirometaxas/periodic-table-cli - #set opts [dict get [punk::args::get_by_id ::textblock::periodic $args] opts] set opts [dict get [punk::args::parse $args withid ::textblock::periodic] opts] set opt_return [tcl::dict::get $opts -return] if {[tcl::dict::get $opts -forcecolour]} { @@ -4446,7 +4467,7 @@ tcl::namespace::eval textblock { proc list_as_table {args} { set FRAMETYPES [textblock::frametypes] - set argd [punk::args::get_by_id ::textblock::list_as_table $args] + set argd [punk::args::parse $args withid ::textblock::list_as_table] set opts [dict get $argd opts] set received [dict get $argd received] @@ -4644,7 +4665,8 @@ tcl::namespace::eval textblock { if {[tcl::string::last \n $charblock] >= 0} { if {$blockwidth > 1} { #set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]] ;#building a repeated "|> command arg" list to evaluate as a pipeline. (from before textblock::join could take arbitrary num of blocks ) - set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + #set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + set row [textblock::join_basic_raw {*}[lrepeat $blockwidth $charblock]] } else { set row $charblock } @@ -4694,7 +4716,7 @@ tcl::namespace::eval textblock { } proc testblock {args} { - set argd [punk::args::get_by_id ::textblock::testblock $args] + set argd [punk::args::parse $args withid ::textblock::testblock] set colour [dict get $argd values colour] set size [dict get $argd opts -size] @@ -4762,7 +4784,8 @@ tcl::namespace::eval textblock { if {"noreset" in $colour} { return [textblock::join_basic -ansiresets 0 -- {*}$clist] } else { - return [textblock::join_basic -- {*}$clist] + #return [textblock::join_basic -- {*}$clist] + return [textblock::join_basic_raw {*}$clist] } } elseif {"rainbow" in $colour} { #direction must be horizontal @@ -5019,19 +5042,20 @@ tcl::namespace::eval textblock { -width ""\ -overflow 0\ -within_ansi 0\ + -return block\ ] #known_samewidth of empty string means we don't know either way, 0 is definitely 'ragged', 1 is definitely homogenous #review!? #-within_ansi means after a leading ansi code when doing left pad on all but last line #-within_ansi means before a trailing ansi code when doing right pad on all but last line - set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0?" foreach {k v} $args { switch -- $k { - -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi { + -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi - -return { tcl::dict::set opts $k $v } default { + set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0? ?-return block|list?" error "textblock::pad unrecognised option '$k'. Usage: $usage" } } @@ -5177,96 +5201,110 @@ tcl::namespace::eval textblock { set line_len 0 set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad foreach {pt ansi} $parts { - if {$pt ne ""} { - set has_nl [expr {[tcl::string::last \n $pt]>=0}] - if {$has_nl} { + if {$pt eq ""} { + #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes + lappend line_chunks "" + } elseif {[tcl::string::last \n $pt]==-1} { + lappend line_chunks $pt + if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + incr line_len [punk::char::grapheme_width_cached $pt] ;#memleak - REVIEW + } + } else { + #set has_nl [expr {[tcl::string::last \n $pt]>=0}] + #if {$has_nl} { set pt [tcl::string::map [list \r\n \n] $pt] set partlines [split $pt \n] - } else { - set partlines [list $pt] - } - set last [expr {[llength $partlines]-1}] - set p 0 - foreach pl $partlines { - lappend line_chunks $pl + #} else { + # set partlines [list $pt] + #} + #set last [expr {[llength $partlines]-1}] + #set p -1 + foreach pl [lrange $partlines 0 end-1] { + #incr p + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. #incr line_len [punk::char::ansifreestring_width $pl] + #if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + # incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + #} + #do padding if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + set missing [expr {$width - $line_len}] + } else { + set missing [expr {$width - $datawidth}] } - if {$p != $last} { - #do padding - if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { - set missing [expr {$width - $line_len}] - } else { - set missing [expr {$width - $datawidth}] - } - if {$missing > 0} { - #commonly in a block - many lines will have the same pad - cache based on missing + if {$missing > 0} { + #commonly in a block - many lines will have the same pad - cache based on missing - #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars - if {[tcl::dict::exists $pad_cache $missing]} { - set pad [tcl::dict::get $pad_cache $missing] + #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] } else { - set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width - if {!$pad_has_ansi} { - set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] - } else { - set base [tcl::string::repeat " " $missing] - set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] - } - dict set pad_cache $missing $pad + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] } - switch -- $which-$opt_withinansi { - r-0 { - lappend line_chunks $pad - } - r-1 { - if {[lindex $line_chunks end] eq ""} { - set line_chunks [linsert $line_chunks end-2 $pad] - } else { - lappend line_chunks $pad - } - } - r-2 { + dict set pad_cache $missing $pad + } + switch -- $which-$opt_withinansi { + r-0 { + lappend line_chunks $pad + } + r-1 { + if {[lindex $line_chunks end] eq ""} { + set line_chunks [linsert $line_chunks end-2 $pad] + } else { lappend line_chunks $pad } - l-0 { - set line_chunks [linsert $line_chunks 0 $pad] + } + r-2 { + lappend line_chunks $pad + } + l-0 { + set line_chunks [linsert $line_chunks 0 $pad] + } + l-1 { + if {[lindex $line_chunks 0] eq ""} { + set line_chunks [linsert $line_chunks 2 $pad] + } else { + set line_chunks [linsert $line_chunks 0 $pad] } - l-1 { + } + l-2 { + if {$lnum == 0} { if {[lindex $line_chunks 0] eq ""} { set line_chunks [linsert $line_chunks 2 $pad] } else { set line_chunks [linsert $line_chunks 0 $pad] } - } - l-2 { - if {$lnum == 0} { - if {[lindex $line_chunks 0] eq ""} { - set line_chunks [linsert $line_chunks 2 $pad] - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } + } else { + set line_chunks [linsert $line_chunks 0 $pad] } } } - lappend lines [::join $line_chunks ""] - set line_chunks [list] - set line_len 0 - incr lnum } - incr p + lappend lines [::join $line_chunks ""] + set line_chunks [list] + set line_len 0 + incr lnum } - } else { - #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes - lappend line_chunks "" + #deal with last part zzz of xxx\nyyy\nzzz - not yet a complete line + set pl [lindex $partlines end] + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. + if {$pl ne "" && ($known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq "")} { + incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + } + } #don't let trailing empty ansi affect the line_chunks length if {$ansi ne ""} { - lappend line_chunks $ansi ;#don't update line_len - review - ansi codes with visible content? + lappend line_chunks $ansi ;#don't update line_len + #- review - ansi codes with visible content? + #- There shouldn't be any, even though for example some terminals display PM content + #e.g OSC 8 is ok as it has the uri 'inside' the ansi sequence, but that's ok because the displayable part is outside and is one of our pt values from split_codes. } } #pad last line @@ -5325,7 +5363,11 @@ tcl::namespace::eval textblock { } } lappend lines [::join $line_chunks ""] - return [::join $lines \n] + if {[tcl::dict::get $opts -return] eq "block"} { + return [::join $lines \n] + } else { + return $lines + } } #left insertion into a list resulting from punk::ansi::ta::split_codes or split_codes_single @@ -5566,7 +5608,7 @@ tcl::namespace::eval textblock { #join without regard to each line length in a block (no padding added to make each block uniform) proc ::textblock::join_basic {args} { - set argd [punk::args::get_by_id ::textblock::join_basic $args] + set argd [punk::args::parse $args withid ::textblock::join_basic] set ansiresets [tcl::dict::get $argd opts -ansiresets] set blocks [tcl::dict::get $argd values blocks] @@ -5602,6 +5644,33 @@ tcl::namespace::eval textblock { } return [::join $outlines \n] } + proc ::textblock::join_basic_raw {args} { + #no options. -*, -- are legimate blocks + set blocklists [lrepeat [llength $args] ""] + set blocklengths [lrepeat [expr {[llength $args]+1}] 0] ;#add 1 to ensure never empty - used only for rowcount max calc + set i -1 + foreach b $args { + incr i + if {[punk::ansi::ta::detect $b]} { + #-ansireplays 1 quite expensive e.g 7ms in 2024 + set blines [punk::lib::lines_as_list -ansireplays 1 -ansiresets auto -- $b] + } else { + set blines [split $b \n] + } + lset blocklengths $i [llength $blines] + lset blocklists $i $blines + } + set rowcount [tcl::mathfunc::max {*}$blocklengths] + set outlines [lrepeat $rowcount ""] + for {set r 0} {$r < $rowcount} {incr r} { + set row "" + foreach blines $blocklists { + append row [lindex $blines $r] + } + lset outlines $r $row + } + return [::join $outlines \n] + } proc ::textblock::join_basic2 {args} { #@cmd -name textblock::join_basic -help "Join blocks line by line but don't add padding on each line to enforce uniform width. # Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner @@ -5686,9 +5755,12 @@ tcl::namespace::eval textblock { } set idx 0 - set blocklists [list] + #set blocklists [list] + set blocklists [lrepeat [llength $blocks] ""] set rowcount 0 + set bidx -1 foreach b $blocks { + incr bidx #we need the width of a rendered block for per-row renderline calls or padding #we may as well use widthinfo to also determine raggedness state to pass on to pad function #set bwidth [width $b] @@ -5705,18 +5777,21 @@ tcl::namespace::eval textblock { if {[punk::ansi::ta::detect $b]} { # - we need to join to use pad - even though we then need to immediately resplit REVIEW (make line list version of pad?) set replay_block [::join [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] \n] - set bl [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $replay_block -return lines -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } else { #each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi - set bl [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $b -return lines -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } - set rowcount [expr {max($rowcount,[llength $bl])}] - lappend blocklists $bl + set rowcount [expr {max($rowcount,[llength $blines])}] + #lappend blocklists $bl + lset blocklists $bidx $blines set width($idx) $bwidth incr idx } - set outlines [list] + set outlines [lrepeat $rowcount ""] for {set r 0} {$r < $rowcount} {incr r} { set row "" for {set c 0} {$c < [llength $blocklists]} {incr c} { @@ -5726,7 +5801,8 @@ tcl::namespace::eval textblock { } append row $cell } - lappend outlines $row + #lappend outlines $row + lset outlines $r $row } return [::join $outlines \n] } @@ -5910,7 +5986,7 @@ tcl::namespace::eval textblock { set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] set ipunks [overtype::renderspace -width [textblock::width $punks] [punk::ansi::enable_inverse]$punks] - set testblock [textblock::testblock 15 rainbow] + set testblock [textblock::testblock -size 15 rainbow] set contents $ansi\n[textblock::join -- " " $table " " $punks " " $testblock " " $ipunks " " $punks] set framed [textblock::frame -checkargs 0 -type arc -title [a+ cyan]Compositing[a] -subtitle [a+ red]ANSI[a] -ansiborder [a+ web-orange] $contents] } @@ -6206,9 +6282,11 @@ tcl::namespace::eval textblock { set spec [string map [list $::textblock::frametypes] { @id -id ::textblock::framedef @cmd -name textblock::framedef\ + -summary "Return frame graphical elements as a dictionary."\ -help "Return a dict of the elements that make up a frame border. May return a subset of available elements based on memberglob values." - + @leaders -min 0 -max 0 + @opts -joins -default "" -type list\ -help "List of join directions, any of: up down left right or those combined with another frametype e.g left-heavy down-light." @@ -6216,7 +6294,7 @@ tcl::namespace::eval textblock { -help "-boxonly true restricts results to the corner,vertical and horizontal box elements It excludes the extra top and side join elements htlj,hlbj,vllj,vlrj." - @values -min 1 + @values -min 1 -max -1 frametype -choices "" -choiceprefix 0 -choicerestricted 0 -type dict\ -help "name from the predefined frametypes or an adhoc dictionary." memberglob -type globstring -optional 1 -multiple 1 -choiceprefix 0 -choicerestricted 0 -choices { @@ -7619,7 +7697,7 @@ tcl::namespace::eval textblock { } -help "Perform an action on the frame cache." } proc frame_cache {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache] set action [dict get $argd values action] variable frame_cache set all_values_dict [dict get $argd values] @@ -7664,7 +7742,7 @@ tcl::namespace::eval textblock { endindex -default "" -type indexexpression } proc frame_cache_display {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache_display $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache_display] variable frame_cache lassign [dict values [dict get $argd values]] startidx endidx set limit "" @@ -7769,75 +7847,93 @@ tcl::namespace::eval textblock { # ${[textblock::frame_samples]} #todo punk::args alias for centre center etc? - punk::args::define { - @dynamic - @id -id ::textblock::frame - @cmd -name "textblock::frame"\ - -help "Frame a block of text with a border." - -checkargs -default 1 -type boolean\ - -help "If true do extra argument checks and - provide more comprehensive error info. - As the argument parser loads around 16 default frame - samples dynamically, this can add add up as each may - take 10s of microseconds. For many-framed tables - and other applications this can add up. - Set false for performance improvement." - -etabs -default 0\ - -help "expanding tabs - experimental/unimplemented." - -type -default light -choices {${[textblock::frametypes]}} -choicerestricted 0 -choicecolumns 8 -type dict\ - -choicelabels { - ${[textblock::frame_samples]} - }\ - -help "Type of border for frame." - -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. - passing an empty string will result in no box, but title/subtitle will still appear if supplied. - ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" - -boxmap -default {} -type dict - -joins -default {} -type list - -title -default "" -type string -regexprefail {\n}\ - -help "Frame title placed on topbar - no newlines. - May contain ANSI - no trailing reset required. - ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing - e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" - -titlealign -default "centre" -choices {left centre right} - -subtitle -default "" -type string -regexprefail {\n}\ - -help "Frame subtitle placed on bottombar - no newlines - May contain Ansi - no trailing reset required." - -subtitlealign -default "centre" -choices {left centre right} - -width -default "" -type int\ - -help "Width of resulting frame including borders. - If omitted or empty-string, the width will be determined automatically based on content." - -height -default "" -type int\ - -help "Height of resulting frame including borders." - -ansiborder -default "" -type ansistring\ - -help "Ansi escape sequence to set border attributes. - ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents - e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" - -ansibase -default "" -type ansistring\ - -help "Default ANSI attributes within frame." - -blockalign -default centre -choices {left right centre}\ - -help "Alignment of the content block within the frame." - -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background - extends within the content block inside the frame. - Has no effect if no ANSI in content." - -textalign -default left -choices {left right centre}\ - -help "Alignment of text within the content block. (centre unimplemented)" - -ellipsis -default 1 -type boolean\ - -help "Whether to show elipsis for truncated content and title/subtitle." - -usecache -default 1 -type boolean - -buildcache -default 1 -type boolean - -crm_mode -default 0 -type boolean\ - -help "Show ANSI control characters within frame contents. - (Control Representation Mode) - Frame width doesn't adapt and content may be truncated - so -width may need to be manually set to display more." + namespace eval argdoc { + punk::args::define { + @dynamic + @id -id ::textblock::frame + @cmd -name "textblock::frame"\ + -summary "Frame a block of content with a border."\ + -help\ + "This command allows content to be framed with various border styles. The content can include + other ANSI codes and unicode characters. Some predefined border types can be selected with + the -type option and the characters can be overridden either in part or in total by supplying + some or all entries in the -boxmap dictionary. + The ${$B}textblock::framedef${$N} command can be used to return a dictionary for a frame type. + Border elements can also be suppressed on chosen sides with -boxlimits. + ANSI colours can be applied to borders or as defaults for the content using -ansiborder and + -ansibase options. + The punk::ansi::a+ function (aliased as a+) can be used to apply ANSI styles. + e.g + frame -type block -ansiborder [a+ blue Red] -ansibase [a+ black Red] \"A\\nB\"" + -checkargs -default 1 -type boolean\ + -help "If true do extra argument checks and + provide more comprehensive error info. + As the argument parser loads around 16 default frame + samples dynamically, this can add add up as each may + take 10s of microseconds. For many-framed tables + and other applications this can add up. + Set false for performance improvement." + -etabs -default 0\ + -help "expanding tabs - experimental/unimplemented." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${[textblock::frametypes]}}\ + -choicerestricted 0 -choicecolumns 8\ + -choicelabels { + ${[textblock::frame_samples]} + }\ + -help "Type of border for frame." + -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. + passing an empty string will result in no box, but title/subtitle will still appear if supplied. + ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" + -boxmap -default {} -type dict + -joins -default {} -type list + -title -default "" -type string -regexprefail {\n}\ + -help "Frame title placed on topbar - no newlines. + May contain ANSI - no trailing reset required. + ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing + e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" + -titlealign -default "centre" -choices {left centre right} + -subtitle -default "" -type string -regexprefail {\n}\ + -help "Frame subtitle placed on bottombar - no newlines + May contain Ansi - no trailing reset required." + -subtitlealign -default "centre" -choices {left centre right} + -width -default "" -type int\ + -help "Width of resulting frame including borders. + If omitted or empty-string, the width will be determined automatically based on content." + -height -default "" -type int\ + -help "Height of resulting frame including borders." + -ansiborder -default "" -type ansistring\ + -help "Ansi escape sequence to set border attributes. + ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents + e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" + -ansibase -default "" -type ansistring\ + -help "Default ANSI attributes within frame." + -blockalign -default centre -choices {left right centre}\ + -help "Alignment of the content block within the frame." + -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background + extends within the content block inside the frame. + Has no effect if no ANSI in content." + -textalign -default left -choices {left right centre}\ + -help "Alignment of text within the content block. (centre unimplemented)" + -ellipsis -default 1 -type boolean\ + -help "Whether to show elipsis for truncated content and title/subtitle." + -usecache -default 1 -type boolean + -buildcache -default 1 -type boolean + -crm_mode -default 0 -type boolean\ + -help "Show ANSI control characters within frame contents. + (Control Representation Mode) + Frame width doesn't adapt and content may be truncated + so -width may need to be manually set to display more." - @values -min 0 -max 1 - contents -default "" -type string\ - -help "Frame contents - may be a block of text containing newlines and ANSI. - Text may be 'ragged' - ie unequal line-lengths. - No trailing ANSI reset required. - ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + @values -min 0 -max 1 + contents -default "" -type string\ + -help "Frame contents - may be a block of text containing newlines and ANSI. + Text may be 'ragged' - ie unequal line-lengths. + No trailing ANSI reset required. + ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + } } #options before content argument - which is allowed to be absent @@ -7886,7 +7982,8 @@ tcl::namespace::eval textblock { if {[lindex $args end-1] eq "--"} { set contents [lpop optlist end] set has_contents 1 - lpop optlist end ;#drop the end-of-opts flag + #lpop optlist end + ledit optlist end end;#drop the end-of-opts flag } else { set optlist $args set contents "" @@ -7928,7 +8025,6 @@ tcl::namespace::eval textblock { #never need to checkargs if only one argument supplied even if it looks like an option - as it will be treated as data to frame if {[llength $args] != 1 && (!$opts_ok || $check_args)} { #as frame is called a lot within table building - checking args can have a *big* impact on final performance. - #set argd [punk::args::get_by_id ::textblock::frame $args] set argd [punk::args::parse $args withid ::textblock::frame] set opts [dict get $argd opts] set contents [dict get $argd values contents] @@ -8530,7 +8626,8 @@ tcl::namespace::eval textblock { #puts "frame--->ansiwrap -rawansi [ansistring VIEW $opt_ansibase] $cache_inner" if {$opt_ansibase ne ""} { if {[punk::ansi::ta::detect $cache_inner]} { - set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + #set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + set cache_inner [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $cache_inner] } else { set cache_inner "$opt_ansibase$cache_inner\x1b\[0m" } @@ -8561,7 +8658,8 @@ tcl::namespace::eval textblock { #JMN test #assert - lhs, cache_inner, rhs non-ragged - so can use join_basic REVIEW #set cache_body [textblock::join -- {*}$cache_bodyparts] - set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + #set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + set cache_body [textblock::join_basic_raw {*}$cache_bodyparts] append fscached $cache_body #append fs $body @@ -8622,7 +8720,8 @@ tcl::namespace::eval textblock { set contents_has_ansi [punk::ansi::ta::detect $contents] if {$opt_ansibase ne ""} { if {$contents_has_ansi} { - set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + #set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + set contents [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $contents] } else { set contents "$opt_ansibase$contents\x1b\[0m" set contents_has_ansi 1 diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index 9809dc62..835fee21 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl @@ -181,16 +181,18 @@ set startdir [pwd] # ------------------------------------------------------------------------------------- set bootsupport_module_paths [list] set bootsupport_library_paths [list] +#we always create these lists in order of desired precedence. +# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add if {[file exists [file join $startdir src bootsupport]]} { + lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] ;#more version-specific modules slightly higher in precedence order lappend bootsupport_module_paths [file join $startdir src bootsupport modules] - lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] ;#more version-specific pkgs slightly higher in precedence order lappend bootsupport_library_paths [file join $startdir src bootsupport lib] - lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] } else { - lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_module_paths [file join $startdir bootsupport modules_tcl$::tclmajorv] - lappend bootsupport_library_paths [file join $startdir bootsupport lib] + lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_library_paths [file join $startdir bootsupport lib_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir bootsupport lib] } set bootsupport_paths_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths] { @@ -210,13 +212,13 @@ set sourcesupport_paths_exist 0 #(most?) Modules in src/modules etc should still be runnable directly in certain cases like this where we point to them. if {[file tail $startdir] eq "src"} { #todo - other src 'module' dirs.. - foreach p [list $startdir/modules $startdir/modules_tcl$::tclmajorv $startdir/vendormodules $startdir/vendormodules_tcl$::tclmajorv] { + foreach p [list $startdir/modules_tcl$::tclmajorv $startdir/modules $startdir/vendormodules_tcl$::tclmajorv $startdir/vendormodules] { if {[file exists $p]} { lappend sourcesupport_module_paths $p } } # -- -- -- - foreach p [list $startdir/lib $startdir/lib_tcl$::tclmajorv $startdir/vendorlib $startdir/vendorlib_tcl$::tclmajorv] { + foreach p [list $startdir/lib_tcl$::tclmajorv $startdir/lib $startdir/vendorlib_tcl$::tclmajorv $startdir/vendorlib] { if {[file exists $p]} { lappend sourcesupport_library_paths $p } @@ -273,16 +275,48 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package forget $pkg } } - #tcl::tm::add {*}$original_tm_list {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - #set ::auto_path [list {*}$original_auto_path {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] - tcl::tm::add {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - set ::auto_path [list {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] + #Deliberately omit original_tm_list and original_auto_path + tcl::tm::add {*}[lreverse $bootsupport_module_paths] {*}[lreverse $sourcesupport_module_paths] ;#tm::add works like LIFO. sourcesupport_module_paths end up earliest in resulting tm list. + set ::auto_path [list {*}$sourcesupport_library_paths {*}$bootsupport_library_paths] + } + puts "----> auto_path $::auto_path" + puts "----> tcl::tm::list [tcl::tm::list]" + + #maint: also in punk::repl package + #-------------------------------------------------------- + set libunks [list] + foreach tm_path [tcl::tm::list] { + set punkdir [file join $tm_path punk] + if {![file exists $punkdir]} {continue} + lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] + } + set libunknown "" + set libunknown_version_sofar "" + foreach lib $libunks { + #expecting to be of form libunknown-.tm + set vtail [lindex [split [file tail $lib] -] 1] + set thisver [file rootname $vtail] ;#file rootname x.y.z.tm + if {$libunknown_version_sofar eq ""} { + set libunknown_version_sofar $thisver + set libunknown $lib + } else { + if {[package vcompare $thisver $libunknown_version_sofar] == 1} { + set libunknown_version_sofar $thisver + set libunknown $lib + } + } } - puts "----> auto_path $::auto_path" - - + if {$libunknown ne ""} { + source $libunknown + if {[catch {punk::libunknown::init -caller main.tcl} errM]} { + puts "error initialising punk::libunknown\n$errM" + } + } + #-------------------------------------------------------- #package require Thread + puts "---->tcl_library [info library]" + puts "---->loaded [info loaded]" # - the full repl requires Threading and punk,shellfilter,shellrun to call and display properly. # tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list @@ -297,6 +331,8 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package require punk::lib package require punk::args package require punk::ansi + package require textblock + set package_paths_modified 1 @@ -1217,15 +1253,20 @@ if {$::punkboot::command eq "check"} { #don't exit yet - 2nd part of "check" below package path restore } # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - Order such that bootsupport entries are always higher priority (if same version number - prefer bootsupport) # - This must be done between the two "check" command sections if {$package_paths_modified} { - set tm_list_now [tcl::tm::list] - foreach p $original_tm_list { - if {$p ni $tm_list_now} { + set tm_list_boot [tcl::tm::list] + tcl::tm::remove {*}$tm_list_boot + foreach p [lreverse $original_tm_list] { + if {$p ni $tm_list_boot} { tcl::tm::add $p } } + foreach p [lreverse $tm_list_boot] { + tcl::tm::add $p + } #set ::auto_path [list $bootsupport_lib {*}$original_auto_path] lappend ::auto_path {*}$original_auto_path } @@ -1333,11 +1374,13 @@ if {$::punkboot::command eq "info"} { if {$::punkboot::command eq "shell"} { + puts stderr ">>>>>> loaded:[info loaded]" package require punk package require punk::repl - puts stderr "punk boot shell not implemented - dropping into ordinary punk shell" - #todo - make procs vars etc from this file available? + puts stderr "punk boot shell not implemented - dropping into ordinary punk shell." + + repl::init repl::start stdin @@ -1504,7 +1547,7 @@ if {$::punkboot::command eq "bootsupport"} { proc modfile_sort {p1 p2} { lassign [split [file rootname $p1] -] _ v1 - lassign [split [file rootname $p1] -] _ v2 + lassign [split [file rootname $p2] -] _ v2 package vcompare $v1 $v2 } proc bootsupport_localupdate {projectroot} { @@ -1543,7 +1586,10 @@ if {$::punkboot::command eq "bootsupport"} { set module_subpath [string map [list :: /] [namespace qualifiers $modulematch]] set srclocation [file join $projectroot $relpath $module_subpath] #puts stdout "$relpath $modulematch $module_subpath $srclocation" - if {[string first - $modulematch]} { + #we must always glob using the dash - or we will match libraries that are suffixes of others + #bare lib.tm with no version is not valid. + if {[string first - $modulematch] != -1} { + #version or part thereof is specified. set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]*.tm] } else { set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]-*.tm] @@ -1566,6 +1612,7 @@ if {$::punkboot::command eq "bootsupport"} { #review set copy_files $pkgmatches } + #if a file added manually to target dir - there will be no .punkcheck record - will be detected as changed foreach cfile $copy_files { set srcfile [file join $srclocation $cfile] set tgtfile [file join $targetroot $module_subpath $cfile] @@ -1574,6 +1621,8 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_init INSTALL $tgtfile $boot_event targetset_addsource $srcfile #---------- + # + #puts "bootsuport target $tgtfile record size: [dict size [$boot_event targetset_last_complete]]" if {\ [llength [dict get [$boot_event targetset_source_changes] changed]]\ || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm index 40366143..b97d1b4e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/argparsingtest-0.1.0.tm @@ -321,6 +321,7 @@ namespace eval argparsingtest { punk::args::define { @id -id ::argparsingtest::test1_punkargs2 @cmd -name argtest4 -help "test of punk::args::parse comparative performance" + @leaders -min 0 -max 0 @opts -anyopts 0 -return -default string -type string -frametype -default \uFFEF -type string @@ -333,10 +334,10 @@ namespace eval argparsingtest { -1 -default 1 -type boolean -2 -default 2 -type integer -3 -default 3 -type integer - @values + @values -min 0 -max 0 } proc test1_punkargs2 {args} { - set argd [punk::args::get_by_id ::argparsingtest::test1_punkargs2 $args] + set argd [punk::args::parse $args withid ::argparsingtest::test1_punkargs2] return [tcl::dict::get $argd opts] } @@ -494,6 +495,38 @@ namespace eval argparsingtest { }]] return $argd } + proc test_multiline2 {args} { + set t3 [textblock::frame t3] + set argd [punk::args::parse $args withdef { + -template1 -default { + ****** + * t1 * + ****** + } + -template2 -default { ------ + ****** + * t2 * + ******} + -template3 -default {$t3} + #substituted or literal values with newlines - no autoindent applied - caller will have to pad appropriately + -template3b -default { + ${$t3} + ----------------- + ${$t3} + abc\ndef + } + -template4 -default "****** + * t4 * + ******" + -template5 -default " + a + ${$t3} + c + " + -flag -default 0 -type boolean + }] + return $argd + } #proc sample1 {p1 n args} { # #*** !doctools diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config index afd1e8f2..226e17de 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config @@ -46,6 +46,7 @@ set bootsupport_modules [list\ modules punkcheck\ modules punkcheck::cli\ modules punk::aliascore\ + modules punk::ansi::colourmap\ modules punk::ansi\ modules punk::assertion\ modules punk::args\ @@ -61,6 +62,7 @@ set bootsupport_modules [list\ modules punk::fileline\ modules punk::docgen\ modules punk::lib\ + modules punk::libunknown\ modules punk::mix\ modules punk::mix::base\ modules punk::mix::cli\ diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm index 5d76af04..d6a9c932 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/pattern-1.2.4.tm @@ -1,1285 +1,1285 @@ -#PATTERN -# - A prototype-based Object system. -# -# Julian Noble 2003 -# License: Public domain -# - -# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. -# -# -# Pattern uses a mixture of class-based and prototype-based object instantiation. -# -# A pattern object has 'properties' and 'methods' -# The system makes a distinction between them with regards to the access syntax for write operations, -# and yet provides unity in access syntax for read operations. -# e.g >object . myProperty -# will return the value of the property 'myProperty' -# >ojbect . myMethod -# will return the result of the method 'myMethod' -# contrast this with the write operations: -# set [>object . myProperty .] blah -# >object . myMethod blah -# however, the property can also be read using: -# set [>object . myProperty .] -# Note the trailing . to give us a sort of 'reference' to the property. -# this is NOT equivalent to -# set [>object . myProperty] -# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property -# i.e it is equivalent in this case to: set blah - -#All objects are represented by a command, the name of which contains a leading ">". -#Any commands in the interp which use this naming convention are assumed to be a pattern object. -#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) - -#All user-added properties & methods of the wrapped object are accessed -# using the separator character "." -#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." -# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) -# you would use the 'Create' metamethod on the pattern object like so: -# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject -# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties -# of the object it was created from. ( - - -#The use of the access-syntax separator character "." allows objects to be kept -# 'clean' in the sense that the only methods &/or properties that can be called this way are ones -# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax -# so you are free to implement your own 'Create' method on your object that doesn't conflict with -# the metamethod. - -#Chainability (or how to violate the Law of Demeter!) -#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other -# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference -# structure, without the need to regress to enter matching brackets as is required when using -# standard TCL command syntax. -# ie instead of: -# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething -# we can use: -# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething -# -# This separates out the object-traversal syntax from the TCL command syntax. - -# . is the 'traversal operator' when it appears between items in a commandlist -# . is the 'reference operator' when it is the last item in a commandlist -# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. -# It marks breaks in the multidimensional structure that correspond to how the data is stored. -# e.g obj . arraydata x y , x1 y1 z1 -# represents an element of a 5-dimensional array structured as a plane of cubes -# e.g2 obj . arraydata x y z , x1 y1 -# represents an element of a 5-dimensional array structured as a cube of planes -# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 -# .. is the 'meta-traversal operator' when it appears between items in a commandlist -# .. is the 'meta-info operator'(?) when it is the last item in a commandlist - - -#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing -# implement iStacks & pStacks (interface stacks & pattern stacks) - -#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 - - -#------------------------------------------------------------ -# System objects. -#------------------------------------------------------------ -#::p::-1 ::p::internals::>metaface -#::p::0 ::p::ifaces::>null -#::p::1 ::>pattern -#------------------------------------------------------------ - -#TODO - -#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) - - -#CHANGES -#2018-09 - v 1.2.2 -# varied refactoring -# Changed invocant datastructure curried into commands (the _ID_ structure) -# Changed MAP structure to dict -# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) -# updated test suites -#2018-08 - v 1.2.1 -# split ::p::predatorX functions into separate files (pkgs) -# e.g patternpredator2-1.0.tm -# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken -# -#2017-08 - v 1.1.6 Fairly big overhaul -# New predator function using coroutines -# Added bang operator ! -# Fixed Constructor chaining -# Added a few tests to test::pattern -# -#2008-03 - preserve ::errorInfo during var writes - -#2007-11 -#Major overhaul + new functionality + new tests v 1.1 -# new dispatch system - 'predator'. -# (preparing for multiple interface stacks, multiple invocants etc) -# -# -#2006-05 -# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. -# -#2005-12 -# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. -# -# Fixed so that PatternVariable default applied on Create. -# -# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: -# - heading towards multiple-interface objects -# -#2005-10-28 -# 1.0.8.1 passes 80/80 tests -# >object .. Destroy - improved cleanup of interfaces & namespaces. -# -#2005-10-26 -# fixes to refsync (still messy!) -# remove variable traces on REF vars during .. Destroy -# passes 76/76 -# -#2005-10-24 -# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. -# 1.0.8.0 now passes 75/76 -# -#2005-10-19 -# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) -# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) -# 1.0.8.0 (passes 74/76) -# tests now in own package -# usage: -# package require test::pattern -# test::p::list -# test::p::run ?nameglob? ?-version ? -# -#2005-09?-12 -# -# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. -# fixed @next@ so that destination method resolved at interface compile time instead of call time -# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. -# (before, the overlay only occured when '.. Method' was used to override.) -# -# -# miscellaneous tidy-ups -# -# 1.0.7.8 (passes 71/73) -# -#2005-09-10 -# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value -# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. -# -#2005-09-07 -# bugfix indexed write to list property -# bugfix Variable default value -# 1.0.7.7 (passes 70/72) -# fails: -# arrayproperty.test - array-entire-reference -# properties.test - property_getter_filter_via_ObjectRef -# -#2005-04-22 -# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) -# -# 1.0.7.4 -# -#2004-11-05 -# basic PropertyRead implementation (non-indexed - no tests!) -# -#2004-08-22 -# object creation speedups - (pattern::internals::obj simplified/indirected) -# -#2004-08-17 -# indexed property setter fixes + tests -# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) -# -#2004-08-16 -# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) -# -#2004-08-15 -# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) -# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger -# - also trigger on curried traces to indexed properties i.e list and array elements. -# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. -# -# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] -# -#2004-08-05 -# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) -# -# fix + add tests to support method & property of same name. (method precedence) -# -#2004-08-04 -# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) -# -# 1.0.7.1 -# use objectref array access to read properties even when some props unset; + test -# unset property using array access on object reference; + test -# -# -#2004-07-21 -# object reference changes - array property values appear as list value when accessed using upvared array. -# bugfixes + tests - properties containing lists (multidimensional access) -# -#1.0.7 -# -#2004-07-20 -# fix default property value append problem -# -#2004-07-17 -# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods -# ( -# -#2004-06-18 -# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. -# -#2004-06-05 -# change argsafety operator to be anything with leading - -# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' -# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, -# the entire dash-prefixed operator is also passed in as an argument. -# e.g >object . doStuff -window . -# will call the doStuff method with the 2 parameters -window . -# >object . doStuff - . -# will call doStuff with single parameter . -# >object . doStuff - -window . -# will result in a reference to the doStuff method with the argument -window 'curried' in. -# -#2004-05-19 -#1.0.6 -# fix so custom constructor code called. -# update Destroy metamethod to unset $self -# -#1.0.4 - 2004-04-22 -# bug fixes regarding method specialisation - added test -# -#------------------------------------------------------------ - -package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] - - -namespace eval pattern::util { - - # Generally better to use 'package require $minver-' - # - this only gives us a different error - proc package_require_min {pkg minver} { - if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { - package require $pkg - } else { - error "Package pattern requires package $pkg of at least version $minver. Available: $available" - } - } -} - -package require patterncmd 1.2.4- -package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) - - - -#package require cmdline -package require overtype - -#package require md5 ;#will be loaded if/when needed -#package require md4 -#package require uuid - - - - - -namespace eval pattern { - variable initialised 0 - - - if 0 { - if {![catch {package require twapi_base} ]} { - #twapi is a windows only package - #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. - # If available - windows seems to provide a fast uuid generator.. - #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) - # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) - interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok - } else { - #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) - # (e.g 200usec 2018 corei9) - #(with or without tcllibc?) - #very first call is extremely slow though - 3.5seconds on 2018 corei9 - package require uuid - interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate - } - #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) - } - - -} - - - - - - -namespace eval p { - #this is also the interp alias namespace. (object commands created here , then renamed into place) - #the object aliases are named as incrementing integers.. !todo - consider uuids? - variable ID 0 - namespace eval internals {} - - - #!?? - #namespace export ?? - variable coroutine_instance 0 -} - -#------------------------------------------------------------------------------------- -#review - what are these for? -#note - this function is deliberately not namespaced -# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features -proc process_pattern_aliases {object args} { - set o [namespace tail $object] - interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] - interp alias {} process_method_$o {} [$object .. Method .] - interp alias {} process_constructor_$o {} [$object .. Constructor .] -} -#------------------------------------------------------------------------------------- - - - - -#!store all interface objects here? -namespace eval ::p::ifaces {} - - - -#K combinator - see http://wiki.tcl.tk/1923 -#proc ::p::K {x y} {set x} -#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] - - - - - - - - -proc ::p::internals::(VIOLATE) {_ID_ violation_script} { - #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] - set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] - - if {![dict get $processed explicitvars]} { - #no explicit var statements - we need the implicit ones - set self [set ::p::${_ID_}::(self)] - set IFID [lindex [set $self] 1 0 end] - #upvar ::p::${IFID}:: self_IFINFO - - - set varDecls {} - set vlist [array get ::p::${IFID}:: v,name,*] - set _k ""; set v "" - if {[llength $vlist]} { - append varDecls "upvar #0 " - foreach {_k v} $vlist { - append varDecls "::p::\${_ID_}::$v $v " - } - append varDecls "\n" - } - - #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] - set violation_script $varDecls\n[dict get $processed body] - - #tidy up - unset processed varDecls self IFID _k v - } else { - set violation_script [dict get $processed body] - } - unset processed - - - - - #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. - eval "unset violation_script;$violation_script" -} - - -proc ::p::internals::DestroyObjectsBelowNamespace {ns} { - #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" - - set nsparts [split [string trim [string map {:: :} $ns] :] :] - if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { - #ns not of form ::p::?::_ref - - foreach obj [info commands ${ns}::>*] { - #catch {::p::meta::Destroy $obj} - #puts ">>found object $obj below ns $ns - destroying $obj" - $obj .. Destroy - } - } - - #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] - #foreach tinfo $traces { - # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo - #} - #unset -nocomplain ${ns}::-->PATTERN_ANCHOR - - foreach sub [namespace children $ns] { - ::p::internals::DestroyObjectsBelowNamespace $sub - } -} - - - - -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# -################################################# - - - - - - - - - -proc ::p::get_new_object_id {} { - tailcall incr ::p::ID - #tailcall ::pattern::new_uuid -} - -#create a new minimal object - with no interfaces or patterns. - -#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} -proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { - - #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" - - if {$OID eq "-2"} { - set OID [::p::get_new_object_id] - #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) - #set OID [pattern::new_uuid] - } - #if $wrapped provided it is assumed to be an existing namespace. - #if {[string length $wrapped]} { - # #??? - #} - - #sanity check - alias must not exist for this OID - if {[llength [interp alias {} ::p::$OID]]} { - error "Object alias '::p::$OID' already exists - cannot create new object with this id" - } - - #system 'varspaces' - - - #until we have a version of Tcl that doesn't have 'creative writing' scope issues - - # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. - # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') - #set o_open 1 - every object is initially also an open interface (?) - #NOTE! comments within namespace eval slow it down. - namespace eval ::p::$OID { - #namespace ensemble create - namespace eval _ref {} - namespace eval _meta {} - namespace eval _iface { - variable o_usedby; - variable o_open 1; - array set o_usedby [list]; - variable o_varspace "" ; - variable o_varspaces [list]; - variable o_methods [dict create]; - variable o_properties [dict create]; - variable o_variables; - variable o_propertyunset_handlers; - set o_propertyunset_handlers [dict create] - } - } - - #set alias ::p::$OID - - #objectid alis default_method object_command wrapped_namespace - set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] - - #MAP is a dict - set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] - - - - #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token - #we've already checked that ::p::$OID doesn't pre-exist - # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias - #interp alias {} ::p::$OID {} ::p::internals::predator $MAP - - - # _ID_ structure - set invocants_dict [dict create this [list $INVOCANTDATA] ] - #puts stdout "New _ID_structure: $interfaces_dict" - set _ID_ [dict create i $invocants_dict context ""] - - - interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ - #rename the command into place - thus the alias & the command name no longer match! - rename ::p::$OID $cmd - - set ::p::${OID}::_meta::map $MAP - - # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something - interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ - - #set p2 [string map {> ?} $cmd] - #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ - - - #trace add command $cmd delete "$cmd .. Destroy ;#" - #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" - - trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" - #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) - - #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" - - - #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" - #trace add command $cmd delete "puts deleting$cmd ;#" - #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" - - - #puts "--> new_object returning map $MAP" - return $MAP -} - - - - -#>x .. Create >y -# ".." is special case equivalent to "._." -# (whereas in theory it would be ".default.") -# "." is equivalent to ".default." is equivalent to ".default.default." (...) - -#>x ._. Create >y -#>x ._.default. Create >y ??? -# -# - -# create object using 'blah' as source interface-stack ? -#>x .blah. .. Create >y -#>x .blah,_. ._. Create .iStackDestination. >y - - - -# -# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] -# the 1st item, blah in this case becomes the 'default' iStack. -# -#>x .*. -# cast to object with all iStacks -# -#>x .*,!_. -# cast to object with all iStacks except _ -# -# --------------------- -#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' -# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. -# -#eg1: >x & >y . some_multi_method arg arg -# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) -# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' -# The invocant signature is thus {these 2} -# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) -# Invocation roles can be specified in the call using the @ operator. -# e.g >x & >y @ points . some_multi_method arg arg -# The invocant signature for this is: {points 2} -# -#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path -# This has the signature {objects n plane 1} where n depends on the length of the list $objects -# -# -# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. -# e.g set pointset [>x & >y .] -# We can now call multimethods on $pointset -# - - - - - - -#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) -proc ::pattern::predatorversion {{ver ""}} { - variable active_predatorversion - set allowed_predatorversions {1 2} - set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions - - if {![info exists active_predatorversion]} { - set first_time_set 1 - } else { - set first_time_set 0 - } - - if {$ver eq ""} { - #get version - if {$first_time_set} { - set active_predatorversions $default_predatorversion - } - return $active_predatorversion - } else { - #set version - if {$ver ni $allowed_predatorversions} { - error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" - } - - if {!$first_time_set} { - if {$active_predatorversion eq $ver} { - #puts stderr "Active predator version is already '$ver'" - #ok - nothing to do - return $active_predatorversion - } else { - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - rename ::p::internals::predator ::p::predator$active_predatorversion - } - } - package require patternpredator$ver 1.2.4- - if {![llength [info commands ::p::predator$ver]]} { - error "Unable to set predatorversion - command ::p::predator$ver not found" - } - - rename ::p::predator$ver ::p::internals::predator - set active_predatorversion $ver - - return $active_predatorversion - } -} -::pattern::predatorversion 2 - - - - - - - - - - - - -# >pattern has object ID 1 -# meta interface has object ID 0 -proc ::pattern::init args { - - if {[set ::pattern::initialised]} { - if {[llength $args]} { - #if callers want to avoid this error, they can do their own check of $::pattern::initialised - error "pattern package is already initialised. Unable to apply args: $args" - } else { - return 1 - } - } - - #this seems out of date. - # - where is PatternPropertyRead? - # - Object is obsolete - # - Coinjoin, Combine don't seem to exist - array set ::p::metaMethods { - Clone object - Conjoin object - Combine object - Create object - Destroy simple - Info simple - Object simple - PatternProperty simple - PatternPropertyWrite simple - PatternPropertyUnset simple - Property simple - PropertyWrite simple - PatternMethod simple - Method simple - PatternVariable simple - Variable simple - Digest simple - PatternUnknown simple - Unknown simple - } - array set ::p::metaProperties { - Properties object - Methods object - PatternProperties object - PatternMethods object - } - - - - - - #create metaface - IID = -1 - also OID = -1 - # all objects implement this special interface - accessed via the .. operator. - - - - - - set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface - - - #OID = 0 - ::p::internals::new_object ::p::ifaces::>null "" 0 - - #? null object has itself as level0 & level1 interfaces? - #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] - - #null interface should always have 'usedby' members. It should never be extended. - array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array - set ::p::0::_iface::o_open 0 - - set ::p::0::_iface::o_constructor [list] - set ::p::0::_iface::o_variables [list] - set ::p::0::_iface::o_properties [dict create] - set ::p::0::_iface::o_methods [dict create] - set ::p::0::_iface::o_varspace "" - set ::p::0::_iface::o_varspaces [list] - array set ::p::0::_iface::o_definition [list] - set ::p::0::_iface::o_propertyunset_handlers [dict create] - - - - - ############################### - # OID = 1 - # >pattern - ############################### - ::p::internals::new_object ::>pattern "" 1 - - #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] - - - array set ::p::1::_iface::o_usedby [list] ;#'usedby' array - - set _self ::pattern - - #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 - #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 - - - - #1)this object references its interfaces - #lappend ID $IFID $IFID_1 - #lset SELFMAP 1 0 $IFID - #lset SELFMAP 2 0 $IFID_1 - - - #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] - #proc ::>pattern args $body - - - - - ####################################################################################### - #OID = 2 - # >ifinfo interface for accessing interfaces. - # - ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object - set ::p::2::_iface::o_constructor [list] - set ::p::2::_iface::o_variables [list] - set ::p::2::_iface::o_properties [dict create] - set ::p::2::_iface::o_methods [dict create] - set ::p::2::_iface::o_varspace "" - set ::p::2::_iface::o_varspaces [list] - array set ::p::2::_iface::o_definition [list] - set ::p::2::_iface::o_open 1 ;#open for extending - - ::p::ifaces::>2 .. AddInterface 2 - - #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations - #(bootstrap because we can't yet use metaface methods on it) - - - - proc ::p::2::_iface::isOpen.1 {_ID_} { - return $::p::2::_iface::o_open - } - interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 - - proc ::p::2::_iface::isClosed.1 {_ID_} { - return [expr {!$::p::2::_iface::o_open}] - } - interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 - - proc ::p::2::_iface::open.1 {_ID_} { - set ::p::2::_iface::o_open 1 - } - interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 - - proc ::p::2::_iface::close.1 {_ID_} { - set ::p::2::_iface::o_open 0 - } - interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 - - - #proc ::p::2::_iface::(GET)properties.1 {_ID_} { - # set ::p::2::_iface::o_properties - #} - #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 - - #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties - - - #proc ::p::2::_iface::(GET)methods.1 {_ID_} { - # set ::p::2::_iface::o_methods - #} - #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 - #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods - - - - - - #link from object to interface (which in this case are one and the same) - - #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] - #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] - #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] - #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] - - interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen - interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed - interp alias {} ::p::2::open {} ::p::2::_iface::open - interp alias {} ::p::2::close {} ::p::2::_iface::close - - - #namespace eval ::p::2 "namespace export $method" - - ####################################################################################### - - - - - - - set ::pattern::initialised 1 - - - ::p::internals::new_object ::p::>interface "" 3 - #create a convenience object on which to manipulate the >ifinfo interface - #set IF [::>pattern .. Create ::p::>interface] - set IF ::p::>interface - - - #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? - # (or is forcing end user to add their own pStack/iStack ok .. ?) - # - ::p::>interface .. AddPatternInterface 2 ;# - - ::p::>interface .. PatternVarspace _iface - - ::p::>interface .. PatternProperty methods - ::p::>interface .. PatternPropertyRead methods {} { - varspace _iface - var {o_methods alias} - return $alias - } - ::p::>interface .. PatternProperty properties - ::p::>interface .. PatternPropertyRead properties {} { - varspace _iface - var o_properties - return $o_properties - } - ::p::>interface .. PatternProperty variables - - ::p::>interface .. PatternProperty varspaces - - ::p::>interface .. PatternProperty definition - - ::p::>interface .. Constructor {{usedbylist {}}} { - #var this - #set this @this@ - #set ns [$this .. Namespace] - #puts "-> creating ns ${ns}::_iface" - #namespace eval ${ns}::_iface {} - - varspace _iface - var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces - - set o_constructor [list] - set o_variables [list] - set o_properties [dict create] - set o_methods [dict create] - set o_varspaces [list] - array set o_definition [list] - - foreach usedby $usedbylist { - set o_usedby(i$usedby) 1 - } - - - } - ::p::>interface .. PatternMethod isOpen {} { - varspace _iface - var o_open - - return $o_open - } - ::p::>interface .. PatternMethod isClosed {} { - varspace _iface - var o_open - - return [expr {!$o_open}] - } - ::p::>interface .. PatternMethod open {} { - varspace _iface - var o_open - set o_open 1 - } - ::p::>interface .. PatternMethod close {} { - varspace _iface - var o_open - set o_open 0 - } - ::p::>interface .. PatternMethod refCount {} { - varspace _iface - var o_usedby - return [array size o_usedby] - } - - set ::p::2::_iface::o_open 1 - - - - - uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} - #uplevel #0 {package require patternlib} - return 1 -} - - - -proc ::p::merge_interface {old new} { - #puts stderr " ** ** ** merge_interface $old $new" - set ns_old ::p::$old - set ns_new ::p::$new - - upvar #0 ::p::${new}:: IFACE - upvar #0 ::p::${old}:: IFACEX - - if {![catch {set c_arglist $IFACEX(c,args)}]} { - #constructor - #for now.. just add newer constructor regardless of any existing one - #set IFACE(c,args) $IFACEX(c,args) - - #if {![info exists IFACE(c,args)]} { - # #target interface didn't have a constructor - # - #} else { - # # - #} - } - - - set methods [::list] - foreach nm [array names IFACEX m-1,name,*] { - lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) - } - - #puts " *** merge interface $old -> $new ****merging-in methods: $methods " - - foreach method $methods { - if {![info exists IFACE(m-1,name,$method)]} { - #target interface doesn't yet have this method - - set THISNAME $method - - if {![string length [info command ${ns_new}::$method]]} { - - if {![set ::p::${old}::_iface::o_open]} { - #interp alias {} ${ns_new}::$method {} ${ns_old}::$method - #namespace eval $ns_new "namespace export [namespace tail $method]" - } else { - #wait to compile - } - - } else { - error "merge interface - command collision " - } - #set i 2 ??? - set i 1 - - } else { - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - - set i [incr IFACE(m-1,chain,$method)] - - set THISNAME ___system___override_${method}_$i - - #move metadata using subindices for delegated methods - set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) - set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) - set IFACE(mp-$i,$method) $IFACE(mp-1,$method) - - set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) - set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) - - - #set next [::p::next_script $IFID0 $method] - if {![string length [info command ${ns_new}::$THISNAME]]} { - if {![set ::p::${old}::_iface::o_open]} { - interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method - namespace eval $ns_new "namespace export $method" - } else { - #wait for compile - } - } else { - error "merge_interface - command collision " - } - - } - - array set IFACE [::list \ - m-1,chain,$method $i \ - m-1,body,$method $IFACEX(m-1,body,$method) \ - m-1,args,$method $IFACEX(m-1,args,$method) \ - m-1,name,$method $THISNAME \ - m-1,iface,$method $old \ - ] - - } - - - - - - #array set ${ns_new}:: [array get ${ns_old}::] - - - #!todo - review - #copy everything else across.. - - foreach {nm v} [array get IFACEX] { - #puts "-.- $nm" - if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { - set IFACE($nm) $v - } - } - - #!todo -write a test - set ::p::${new}::_iface::o_open 1 - - #!todo - is this done also when iface compiled? - #namespace eval ::p::$new {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place - - return -} - - - - -#detect attempt to treat a reference to a method as a property -proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { -#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" - lassign [lrange $args end-2 end] vtraced vidx op - #NOTE! cannot rely on vtraced as it may have been upvared - - switch -- $op { - write { - error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - unset { - #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace - #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #!todo - don't use vtraced! - trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] - - #pointless raising an error as "Any errors in unset traces are ignored" - #error "cannot unset. $field is a method not a property" - } - read { - error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" - } - array { - error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" - #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" - } - } - - return -} - - - - -#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. -# -# The 'dispatcher' is an object instance's underlying object command. -# - -#proc ::p::make_dispatcher {obj ID IFID} { -# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { -# ::p::@IID@ $methprop @oid@ {*}$args -# }] -# return -#} - - - - -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -#aliased from ::p::${OID}:: -# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something -proc ::p::internals::no_default_method {_ID_ args} { - puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" - lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped - tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" -} - -#force 1 will extend an interface even if shared. (??? why is this necessary here?) -#if IID empty string - create the interface. -proc ::p::internals::expand_interface {IID {force 0}} { - #puts stdout ">>> expand_interface $IID [info level -1]<<<" - if {![string length $IID]} { - #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) - set iid [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$iid - return $iid - } else { - if {[set ::p::${IID}::_iface::o_open]} { - #interface open for extending - shared or not! - return $IID - } - - if {[array size ::p::${IID}::_iface::o_usedby] > 1} { - #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby - - #oops.. shared interface. Copy before specialising it. - set prev_IID $IID - - #set IID [::p::internals::new_interface] - set IID [expr {$::p::ID + 1}] - ::p::>interface .. Create ::p::ifaces::>$IID - - ::p::internals::linkcopy_interface $prev_IID $IID - #assert: prev_usedby contains at least one other element. - } - - #whether copied or not - mark as open for extending. - set ::p::${IID}::_iface::o_open 1 - return $IID - } -} - -#params: old - old (shared) interface ID -# new - new interface ID -proc ::p::internals::linkcopy_interface {old new} { - #puts stderr " ** ** ** linkcopy_interface $old $new" - set ns_old ::p::${old}::_iface - set ns_new ::p::${new}::_iface - - - - foreach nsmethod [info commands ${ns_old}::*.1] { - #puts ">>> adding $nsmethod to iface $new" - set tail [namespace tail $nsmethod] - set method [string range $tail 0 end-2] ;#strip .1 - - if {![llength [info commands ${ns_new}::$method]]} { - - set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 - - #link from new interface namespace to existing one. - #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) - #!todo? verify? - #- actual link is chainslot to chainslot - interp alias {} ${ns_new}::$method.1 {} $oldhead - - #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? - - - #chainhead pointer within new interface - interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 - - namespace eval $ns_new "namespace export $method" - - #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { - # lappend ${ns_new}::o_methods $method - #} - } else { - if {$method eq "(VIOLATE)"} { - #ignore for now - #!todo - continue - } - - #!todo - handle how? - #error "command $cmd already exists in interface $new" - - #warning - existing chainslot will be completely shadowed by linked method. - # - existing one becomes unreachable. #!todo review!? - - - error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" - - } - } - - - #foreach propinf [set ${ns_old}::o_properties] { - # lassign $propinf prop _default - # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop - # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop - # lappend ${ns_new}::o_properties $propinf - #} - - - set ${ns_new}::o_variables [set ${ns_old}::o_variables] - set ${ns_new}::o_properties [set ${ns_old}::o_properties] - set ${ns_new}::o_methods [set ${ns_old}::o_methods] - set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] - - - set ::p::${old}::_iface::o_usedby(i$new) linkcopy - - - #obsolete.? - array set ::p::${new}:: [array get ::p::${old}:: ] - - - - #!todo - is this done also when iface compiled? - #namespace eval ::p::${new}::_iface {namespace ensemble create} - - - #puts stderr "copy_interface $old $new" - - #assume that the (usedby) data is now obsolete - #???why? - #set ${ns_new}::(usedby) [::list] - - #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' - - return -} -################################################################################################################################################ -################################################################################################################################################ -################################################################################################################################################ - -pattern::init - -return $::pattern::version +#PATTERN +# - A prototype-based Object system. +# +# Julian Noble 2003 +# License: Public domain +# + +# "I need pattern" - Lexx Series 1 Episode 3 - Eating Pattern. +# +# +# Pattern uses a mixture of class-based and prototype-based object instantiation. +# +# A pattern object has 'properties' and 'methods' +# The system makes a distinction between them with regards to the access syntax for write operations, +# and yet provides unity in access syntax for read operations. +# e.g >object . myProperty +# will return the value of the property 'myProperty' +# >ojbect . myMethod +# will return the result of the method 'myMethod' +# contrast this with the write operations: +# set [>object . myProperty .] blah +# >object . myMethod blah +# however, the property can also be read using: +# set [>object . myProperty .] +# Note the trailing . to give us a sort of 'reference' to the property. +# this is NOT equivalent to +# set [>object . myProperty] +# This last example is of course calling set against a standard variable whose name is whatever value is returned by reading the property +# i.e it is equivalent in this case to: set blah + +#All objects are represented by a command, the name of which contains a leading ">". +#Any commands in the interp which use this naming convention are assumed to be a pattern object. +#Use of non-pattern commands containing this leading character is not supported. (Behaviour is undefined) + +#All user-added properties & methods of the wrapped object are accessed +# using the separator character "." +#Metamethods supplied by the patterm system are accessed with the object command using the metamethod separator ".." +# e.g to instantiate a new object from an existing 'pattern' (the equivalent of a class or prototype) +# you would use the 'Create' metamethod on the pattern object like so: +# >MyFactoryClassOrPrototypeLikeThing .. Create >NameOfNewObject +# '>NameOfNewObject' is now available as a command, with certain inherited methods and properties +# of the object it was created from. ( + + +#The use of the access-syntax separator character "." allows objects to be kept +# 'clean' in the sense that the only methods &/or properties that can be called this way are ones +# the programmer(you!) put there. Existing metamethods such as 'Create' are accessed using a different syntax +# so you are free to implement your own 'Create' method on your object that doesn't conflict with +# the metamethod. + +#Chainability (or how to violate the Law of Demeter!) +#The . access-syntax gives TCL an OO syntax more closely in line with many OO systems in other +# languages such as Python & VB, and allows left to right keyboard-entry of a deeply nested object-reference +# structure, without the need to regress to enter matching brackets as is required when using +# standard TCL command syntax. +# ie instead of: +# [[[object nextObject] getItem 4] getItem [chooseItemNumber]] doSomething +# we can use: +# >object . nextObject . getItem 4 . getItem [chooseItemNumber] . doSomething +# +# This separates out the object-traversal syntax from the TCL command syntax. + +# . is the 'traversal operator' when it appears between items in a commandlist +# . is the 'reference operator' when it is the last item in a commandlist +# , is the 'index traversal operator' (or 'nest operator') - mathematically it marks where there is a matrix 'partition'. +# It marks breaks in the multidimensional structure that correspond to how the data is stored. +# e.g obj . arraydata x y , x1 y1 z1 +# represents an element of a 5-dimensional array structured as a plane of cubes +# e.g2 obj . arraydata x y z , x1 y1 +# represents an element of a 5-dimensional array structured as a cube of planes +# The underlying storage for e.g2 might consist of something such as a Tcl array indexed such as cube($x,$y,$z) where each value is a patternlib::>matrix object with indices x1 y1 +# .. is the 'meta-traversal operator' when it appears between items in a commandlist +# .. is the 'meta-info operator'(?) when it is the last item in a commandlist + + +#!todo - Duck Typing: http://en.wikipedia.org/wiki/Duck_typing +# implement iStacks & pStacks (interface stacks & pattern stacks) + +#see also: Using namsepace ensemble without a namespace: http://wiki.tcl.tk/16975 + + +#------------------------------------------------------------ +# System objects. +#------------------------------------------------------------ +#::p::-1 ::p::internals::>metaface +#::p::0 ::p::ifaces::>null +#::p::1 ::>pattern +#------------------------------------------------------------ + +#TODO + +#investigate use of [namespace path ... ] to resolve command lookup (use it to chain iStacks?) + + +#CHANGES +#2018-09 - v 1.2.2 +# varied refactoring +# Changed invocant datastructure curried into commands (the _ID_ structure) +# Changed MAP structure to dict +# Default Method no longer magic "item" - must be explicitly set with .. DefaultMethod (or .. PatternDefaultMethod for patterns) +# updated test suites +#2018-08 - v 1.2.1 +# split ::p::predatorX functions into separate files (pkgs) +# e.g patternpredator2-1.0.tm +# patternpredator1-1.0 - split out but not updated/tested - probably obsolete and very broken +# +#2017-08 - v 1.1.6 Fairly big overhaul +# New predator function using coroutines +# Added bang operator ! +# Fixed Constructor chaining +# Added a few tests to test::pattern +# +#2008-03 - preserve ::errorInfo during var writes + +#2007-11 +#Major overhaul + new functionality + new tests v 1.1 +# new dispatch system - 'predator'. +# (preparing for multiple interface stacks, multiple invocants etc) +# +# +#2006-05 +# Adjusted 'var' expansion to use the new tcl8.5 'namespace upvar $ns v1 n1 v2 n2 ... ' feature. +# +#2005-12 +# Adjusted 'var' expansion in method/constructor etc bodies to be done 'inline' where it appears rather than aggregated at top. +# +# Fixed so that PatternVariable default applied on Create. +# +# unified interface/object datastructures under ::p:::: instead of seperate ::p::IFACE:::: +# - heading towards multiple-interface objects +# +#2005-10-28 +# 1.0.8.1 passes 80/80 tests +# >object .. Destroy - improved cleanup of interfaces & namespaces. +# +#2005-10-26 +# fixes to refsync (still messy!) +# remove variable traces on REF vars during .. Destroy +# passes 76/76 +# +#2005-10-24 +# fix objectRef_TraceHandler so that reading a property via an object reference using array syntax will call a PropertyRead function if defined. +# 1.0.8.0 now passes 75/76 +# +#2005-10-19 +# Command alias introduced by @next@ is now placed in the interfaces namespace. (was unnamespaced before) +# changed IFACE array names for level0 methods to be m-1 instead of just m. (now consistent with higher level m-X names) +# 1.0.8.0 (passes 74/76) +# tests now in own package +# usage: +# package require test::pattern +# test::p::list +# test::p::run ?nameglob? ?-version ? +# +#2005-09?-12 +# +# fixed standalone 'var' statement in method bodies so that no implicit variable declarations added to proc. +# fixed @next@ so that destination method resolved at interface compile time instead of call time +# fixed @next@ so that on Create, .. PatternMethod x overlays existing method produced by a previous .. PatternMethod x. +# (before, the overlay only occured when '.. Method' was used to override.) +# +# +# miscellaneous tidy-ups +# +# 1.0.7.8 (passes 71/73) +# +#2005-09-10 +# fix 'unknown' system such that unspecified 'unknown' handler represented by lack of (unknown) variable instead of empty string value +# this is so that a mixin with an unspecified 'unknown' handler will not undo a lowerlevel 'unknown' specificier. +# +#2005-09-07 +# bugfix indexed write to list property +# bugfix Variable default value +# 1.0.7.7 (passes 70/72) +# fails: +# arrayproperty.test - array-entire-reference +# properties.test - property_getter_filter_via_ObjectRef +# +#2005-04-22 +# basic fix to PatternPropertyRead dispatch code - updated tests (indexed case still not fixed!) +# +# 1.0.7.4 +# +#2004-11-05 +# basic PropertyRead implementation (non-indexed - no tests!) +# +#2004-08-22 +# object creation speedups - (pattern::internals::obj simplified/indirected) +# +#2004-08-17 +# indexed property setter fixes + tests +# meta::Create fixes - state preservation on overlay (correct constructor called, property defaults respect existing values) +# +#2004-08-16 +# PropertyUnset & PatternPropertyUnset metaMethods (filter method called on property unset) +# +#2004-08-15 +# reference syncing: ensure writes to properties always trigger traces on property references (+ tests) +# - i.e method that updates o_myProp var in >myObj will cause traces on [>myObj . myProp .] to trigger +# - also trigger on curried traces to indexed properties i.e list and array elements. +# - This feature presumably adds some overhead to all property writes - !todo - investigate desirability of mechanism to disable on specific properties. +# +# fix (+ tests) for ref to multiple indices on object i.e [>myObj key1 key2 .] +# +#2004-08-05 +# add PropertyWrite & PatternPropertyWrite metaMethods - (filter method called on property write) +# +# fix + add tests to support method & property of same name. (method precedence) +# +#2004-08-04 +# disallow attempt to use method reference as if it were a property (raise error instead of silently setting useless var) +# +# 1.0.7.1 +# use objectref array access to read properties even when some props unset; + test +# unset property using array access on object reference; + test +# +# +#2004-07-21 +# object reference changes - array property values appear as list value when accessed using upvared array. +# bugfixes + tests - properties containing lists (multidimensional access) +# +#1.0.7 +# +#2004-07-20 +# fix default property value append problem +# +#2004-07-17 +# add initial implementation of 'Unknown' and 'PatternUnknown' meta-methods +# ( +# +#2004-06-18 +# better cleanup on '>obj .. Destroy' - recursively destroy objects under parents subnamespaces. +# +#2004-06-05 +# change argsafety operator to be anything with leading - +# if standalone '-' then the dash itself is not added as a parameter, but if a string follows '-' +# i.e tkoption style; e.g -myoption ; then in addition to acting as an argsafety operator for the following arg, +# the entire dash-prefixed operator is also passed in as an argument. +# e.g >object . doStuff -window . +# will call the doStuff method with the 2 parameters -window . +# >object . doStuff - . +# will call doStuff with single parameter . +# >object . doStuff - -window . +# will result in a reference to the doStuff method with the argument -window 'curried' in. +# +#2004-05-19 +#1.0.6 +# fix so custom constructor code called. +# update Destroy metamethod to unset $self +# +#1.0.4 - 2004-04-22 +# bug fixes regarding method specialisation - added test +# +#------------------------------------------------------------ + +package provide pattern [namespace eval pattern {variable version; set version 1.2.4}] + + +namespace eval pattern::util { + + # Generally better to use 'package require $minver-' + # - this only gives us a different error + proc package_require_min {pkg minver} { + if {[package vsatisfies [lindex [set available [lsort -increasing [package versions $pkg]]] end] $minver-]} { + package require $pkg + } else { + error "Package pattern requires package $pkg of at least version $minver. Available: $available" + } + } +} + +package require patterncmd 1.2.4- +package require metaface 1.2.4- ;#utility/system diagnostic commands (may be used by metaface lib etc) + + + +#package require cmdline +package require overtype + +#package require md5 ;#will be loaded if/when needed +#package require md4 +#package require uuid + + + + + +namespace eval pattern { + variable initialised 0 + + + if 0 { + if {![catch {package require twapi_base} ]} { + #twapi is a windows only package + #MUCH faster to load just twapi_base than full 'package require twapi' IFF using the modular twapi distribution with multiple separately loadable dlls. + # If available - windows seems to provide a fast uuid generator.. + #*IF* tcllibc is missing, then as at 2008-05 twapi::new_uuid is significantly faster than uuid::uuid generate ( e.g 19 usec vs 76thousand usec! on 2.4GHZ machine) + # (2018 update - 15-30usec vs ~200usec on core i9 @ ~2.6GHZ (time for a single call e.g time {pattern::new_uuid})) + interp alias {} ::pattern::new_uuid {} ::twapi::new_uuid -localok + } else { + #performance on freebsd seems not great, but adequate. (e.g 500usec on dualcore 1.6GHZ) + # (e.g 200usec 2018 corei9) + #(with or without tcllibc?) + #very first call is extremely slow though - 3.5seconds on 2018 corei9 + package require uuid + interp alias {} ::pattern::new_uuid {} ::uuid::uuid generate + } + #variable fastobj 0 ;#precalculated invocant ID in method body (instead of call time ) - removed for now - see pattern 1.2.1 (a premature optimisation which was hampering refactoring & advancement) + } + + +} + + + + + + +namespace eval p { + #this is also the interp alias namespace. (object commands created here , then renamed into place) + #the object aliases are named as incrementing integers.. !todo - consider uuids? + variable ID 0 + namespace eval internals {} + + + #!?? + #namespace export ?? + variable coroutine_instance 0 +} + +#------------------------------------------------------------------------------------- +#review - what are these for? +#note - this function is deliberately not namespaced +# - it begins with the letters 'proc' (as do the created aliases) - to aid in editor's auto indexing/mapping features +proc process_pattern_aliases {object args} { + set o [namespace tail $object] + interp alias {} process_patternmethod_$o {} [$object .. PatternMethod .] + interp alias {} process_method_$o {} [$object .. Method .] + interp alias {} process_constructor_$o {} [$object .. Constructor .] +} +#------------------------------------------------------------------------------------- + + + + +#!store all interface objects here? +namespace eval ::p::ifaces {} + + + +#K combinator - see http://wiki.tcl.tk/1923 +#proc ::p::K {x y} {set x} +#- not used - use inline K if desired i.e set x [lreplace $x[set x{}] $a $b blah] + + + + + + + + +proc ::p::internals::(VIOLATE) {_ID_ violation_script} { + #set out [::p::fixed_var_statements @IMPLICITDECLS@\n$violation_script] + set processed [dict create {*}[::p::predator::expand_var_statements $violation_script]] + + if {![dict get $processed explicitvars]} { + #no explicit var statements - we need the implicit ones + set self [set ::p::${_ID_}::(self)] + set IFID [lindex [set $self] 1 0 end] + #upvar ::p::${IFID}:: self_IFINFO + + + set varDecls {} + set vlist [array get ::p::${IFID}:: v,name,*] + set _k ""; set v "" + if {[llength $vlist]} { + append varDecls "upvar #0 " + foreach {_k v} $vlist { + append varDecls "::p::\${_ID_}::$v $v " + } + append varDecls "\n" + } + + #set violation_script [string map [::list @IMPLICITDECLS@ $varDecls] $out] + set violation_script $varDecls\n[dict get $processed body] + + #tidy up + unset processed varDecls self IFID _k v + } else { + set violation_script [dict get $processed body] + } + unset processed + + + + + #!todo - review (& document) exactly what context this script runs in and what vars/procs are/should be visible. + eval "unset violation_script;$violation_script" +} + + +proc ::p::internals::DestroyObjectsBelowNamespace {ns} { + #puts "\n##################\n#################### destroyObjectsBelowNamespace $ns\n" + + set nsparts [split [string trim [string map {:: :} $ns] :] :] + if { ! ( ([llength $nsparts] == 3) & ([lindex $nsparts 0] == "p") & ([lindex $nsparts end] eq "_ref") )} { + #ns not of form ::p::?::_ref + + foreach obj [info commands ${ns}::>*] { + #catch {::p::meta::Destroy $obj} + #puts ">>found object $obj below ns $ns - destroying $obj" + $obj .. Destroy + } + } + + #set traces [trace info variable ${ns}::-->PATTERN_ANCHOR] + #foreach tinfo $traces { + # trace remove variable ${ns}::-->PATTERN_ANCHOR {*}$tinfo + #} + #unset -nocomplain ${ns}::-->PATTERN_ANCHOR + + foreach sub [namespace children $ns] { + ::p::internals::DestroyObjectsBelowNamespace $sub + } +} + + + + +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# +################################################# + + + + + + + + + +proc ::p::get_new_object_id {} { + tailcall incr ::p::ID + #tailcall ::pattern::new_uuid +} + +#create a new minimal object - with no interfaces or patterns. + +#proc ::p::internals::new_object [list cmd {wrapped ""} [list OID [expr {-2}]]] {} +proc ::p::internals::new_object {cmd {wrapped ""} {OID "-2"}} { + + #puts "-->new_object cmd:$cmd wrapped:$wrapped OID:$OID" + + if {$OID eq "-2"} { + set OID [::p::get_new_object_id] + #set OID [incr ::p::ID] ;#!todo - use uuids? (too slow?) (use uuids as configurable option?, pre-allocate a list of uuids?) + #set OID [pattern::new_uuid] + } + #if $wrapped provided it is assumed to be an existing namespace. + #if {[string length $wrapped]} { + # #??? + #} + + #sanity check - alias must not exist for this OID + if {[llength [interp alias {} ::p::$OID]]} { + error "Object alias '::p::$OID' already exists - cannot create new object with this id" + } + + #system 'varspaces' - + + #until we have a version of Tcl that doesn't have 'creative writing' scope issues - + # - we should either explicity specify the whole namespace when setting variables or make sure we use the 'variable' keyword. + # (see http://wiki.tcl.tk/1030 'Dangers of creative writing') + #set o_open 1 - every object is initially also an open interface (?) + #NOTE! comments within namespace eval slow it down. + namespace eval ::p::$OID { + #namespace ensemble create + namespace eval _ref {} + namespace eval _meta {} + namespace eval _iface { + variable o_usedby; + variable o_open 1; + array set o_usedby [list]; + variable o_varspace "" ; + variable o_varspaces [list]; + variable o_methods [dict create]; + variable o_properties [dict create]; + variable o_variables; + variable o_propertyunset_handlers; + set o_propertyunset_handlers [dict create] + } + } + + #set alias ::p::$OID + + #objectid alis default_method object_command wrapped_namespace + set INVOCANTDATA [list $OID ::p::$OID "" $cmd $wrapped] + + #MAP is a dict + set MAP [list invocantdata $INVOCANTDATA interfaces {level0 {} level0_default "" level1 {} level1_default ""} patterndata {patterndefaultmethod ""}] + + + + #NOTE 'interp alias' will prepend :: if chosen srccmd already exists as an alias token + #we've already checked that ::p::$OID doesn't pre-exist + # - so we know the return value of the [interp alias {} $alias {} ...] will be $alias + #interp alias {} ::p::$OID {} ::p::internals::predator $MAP + + + # _ID_ structure + set invocants_dict [dict create this [list $INVOCANTDATA] ] + #puts stdout "New _ID_structure: $interfaces_dict" + set _ID_ [dict create i $invocants_dict context ""] + + + interp alias {} ::p::$OID {} ::p::internals::predator $_ID_ + #rename the command into place - thus the alias & the command name no longer match! + rename ::p::$OID $cmd + + set ::p::${OID}::_meta::map $MAP + + # called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something + interp alias {} ::p::${OID}:: {} ::p::internals::no_default_method $_ID_ + + #set p2 [string map {> ?} $cmd] + #interp alias {} $p2 {} ::p::internals::alternative_predator $_ID_ + + + #trace add command $cmd delete "$cmd .. Destroy ;#" + #puts "@@@ trace add command $cmd rename [list $cmd .. Rename]" + + trace add command $cmd rename [list $cmd .. Rename] ;#will receive $oldname $newname "rename" + #trace add command $cmd rename [$cmd .. Rename .] ;#EXTREMELY slow. (but why?) + + #puts "@@@ trace added for $cmd -> '[trace info command $cmd]'" + + + #uplevel #0 "trace add command $cmd delete \"puts deleting$cmd ;#\"" + #trace add command $cmd delete "puts deleting$cmd ;#" + #puts stdout "trace add command $cmd delete \"puts deleting$cmd ;#\"" + + + #puts "--> new_object returning map $MAP" + return $MAP +} + + + + +#>x .. Create >y +# ".." is special case equivalent to "._." +# (whereas in theory it would be ".default.") +# "." is equivalent to ".default." is equivalent to ".default.default." (...) + +#>x ._. Create >y +#>x ._.default. Create >y ??? +# +# + +# create object using 'blah' as source interface-stack ? +#>x .blah. .. Create >y +#>x .blah,_. ._. Create .iStackDestination. >y + + + +# +# ">x .blah,_." is a reference(cast) to >x that contains only the iStacks in the order listed. i.e [list blah _] +# the 1st item, blah in this case becomes the 'default' iStack. +# +#>x .*. +# cast to object with all iStacks +# +#>x .*,!_. +# cast to object with all iStacks except _ +# +# --------------------- +#!todo - MultiMethod support via transient and persistent object conglomerations. Operators '&' & '@' +# - a persistent conglomeration will have an object id (OID) and thus associated namespace, whereas a transient one will not. +# +#eg1: >x & >y . some_multi_method arg arg +# this is a call to the MultiMethod 'some_multi_method' with 2 objects as the invocants. ('>x & >y' is a transient conglomeration of the two objects) +# No explicit 'invocation role' is specified in this call - so it gets the default role for multiple invocants: 'these' +# The invocant signature is thus {these 2} +# (the default invocation role for a standard call on a method with a single object is 'this' - with the associated signature {this 1}) +# Invocation roles can be specified in the call using the @ operator. +# e.g >x & >y @ points . some_multi_method arg arg +# The invocant signature for this is: {points 2} +# +#eg2: {*}[join $objects &] @ objects & >p @ plane . move $path +# This has the signature {objects n plane 1} where n depends on the length of the list $objects +# +# +# To get a persistent conglomeration we would need to get a 'reference' to the conglomeration. +# e.g set pointset [>x & >y .] +# We can now call multimethods on $pointset +# + + + + + + +#set ::p::internals::predator to a particular predatorversion (from a patternpredatorX package) +proc ::pattern::predatorversion {{ver ""}} { + variable active_predatorversion + set allowed_predatorversions {1 2} + set default_predatorversion [lindex $allowed_predatorversions end] ;#default to last in list of allowed_predatorversions + + if {![info exists active_predatorversion]} { + set first_time_set 1 + } else { + set first_time_set 0 + } + + if {$ver eq ""} { + #get version + if {$first_time_set} { + set active_predatorversions $default_predatorversion + } + return $active_predatorversion + } else { + #set version + if {$ver ni $allowed_predatorversions} { + error "Invalid attempt to set predatorversion - unknown value: $ver, try one of: $allowed_predatorversions" + } + + if {!$first_time_set} { + if {$active_predatorversion eq $ver} { + #puts stderr "Active predator version is already '$ver'" + #ok - nothing to do + return $active_predatorversion + } else { + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + rename ::p::internals::predator ::p::predator$active_predatorversion + } + } + package require patternpredator$ver 1.2.4- + if {![llength [info commands ::p::predator$ver]]} { + error "Unable to set predatorversion - command ::p::predator$ver not found" + } + + rename ::p::predator$ver ::p::internals::predator + set active_predatorversion $ver + + return $active_predatorversion + } +} +::pattern::predatorversion 2 + + + + + + + + + + + + +# >pattern has object ID 1 +# meta interface has object ID 0 +proc ::pattern::init args { + + if {[set ::pattern::initialised]} { + if {[llength $args]} { + #if callers want to avoid this error, they can do their own check of $::pattern::initialised + error "pattern package is already initialised. Unable to apply args: $args" + } else { + return 1 + } + } + + #this seems out of date. + # - where is PatternPropertyRead? + # - Object is obsolete + # - Coinjoin, Combine don't seem to exist + array set ::p::metaMethods { + Clone object + Conjoin object + Combine object + Create object + Destroy simple + Info simple + Object simple + PatternProperty simple + PatternPropertyWrite simple + PatternPropertyUnset simple + Property simple + PropertyWrite simple + PatternMethod simple + Method simple + PatternVariable simple + Variable simple + Digest simple + PatternUnknown simple + Unknown simple + } + array set ::p::metaProperties { + Properties object + Methods object + PatternProperties object + PatternMethods object + } + + + + + + #create metaface - IID = -1 - also OID = -1 + # all objects implement this special interface - accessed via the .. operator. + + + + + + set ::p::ID 4 ;#0,1,2,3 reserved for null interface,>pattern, >ifinfo & ::p::>interface + + + #OID = 0 + ::p::internals::new_object ::p::ifaces::>null "" 0 + + #? null object has itself as level0 & level1 interfaces? + #set ::p::ifaces::>null [list [list 0 ::p::ifaces::>null item] [list [list 0] [list 0]] [list {} {}]] + + #null interface should always have 'usedby' members. It should never be extended. + array set ::p::0::_iface::o_usedby [list i-1 ::p::internals::>metaface i0 ::p::ifaces::>null i1 ::>pattern] ;#'usedby' array + set ::p::0::_iface::o_open 0 + + set ::p::0::_iface::o_constructor [list] + set ::p::0::_iface::o_variables [list] + set ::p::0::_iface::o_properties [dict create] + set ::p::0::_iface::o_methods [dict create] + set ::p::0::_iface::o_varspace "" + set ::p::0::_iface::o_varspaces [list] + array set ::p::0::_iface::o_definition [list] + set ::p::0::_iface::o_propertyunset_handlers [dict create] + + + + + ############################### + # OID = 1 + # >pattern + ############################### + ::p::internals::new_object ::>pattern "" 1 + + #set ::>pattern [list [list 1 ::>pattern item] [list [list 0] [list 0]]] + + + array set ::p::1::_iface::o_usedby [list] ;#'usedby' array + + set _self ::pattern + + #set IFID [::p::internals::new_interface 1] ;#level 0 interface usedby object 1 + #set IFID_1 [::p::internals::new_interface 1] ;#level 1 interface usedby object 1 + + + + #1)this object references its interfaces + #lappend ID $IFID $IFID_1 + #lset SELFMAP 1 0 $IFID + #lset SELFMAP 2 0 $IFID_1 + + + #set body [string map [::list @self@ ::>pattern @_self@ ::pattern @self_ID@ 0 @itemCmd@ item] $::p::internals::OBJECTCOMMAND] + #proc ::>pattern args $body + + + + + ####################################################################################### + #OID = 2 + # >ifinfo interface for accessing interfaces. + # + ::p::internals::new_object ::p::ifaces::>2 "" 2 ;#>ifinfo object + set ::p::2::_iface::o_constructor [list] + set ::p::2::_iface::o_variables [list] + set ::p::2::_iface::o_properties [dict create] + set ::p::2::_iface::o_methods [dict create] + set ::p::2::_iface::o_varspace "" + set ::p::2::_iface::o_varspaces [list] + array set ::p::2::_iface::o_definition [list] + set ::p::2::_iface::o_open 1 ;#open for extending + + ::p::ifaces::>2 .. AddInterface 2 + + #Manually create a minimal >ifinfo implementation using the same general pattern we use for all method implementations + #(bootstrap because we can't yet use metaface methods on it) + + + + proc ::p::2::_iface::isOpen.1 {_ID_} { + return $::p::2::_iface::o_open + } + interp alias {} ::p::2::_iface::isOpen {} ::p::2::_iface::isOpen.1 + + proc ::p::2::_iface::isClosed.1 {_ID_} { + return [expr {!$::p::2::_iface::o_open}] + } + interp alias {} ::p::2::_iface::isClosed {} ::p::2::_iface::isClosed.1 + + proc ::p::2::_iface::open.1 {_ID_} { + set ::p::2::_iface::o_open 1 + } + interp alias {} ::p::2::_iface::open {} ::p::2::_iface::open.1 + + proc ::p::2::_iface::close.1 {_ID_} { + set ::p::2::_iface::o_open 0 + } + interp alias {} ::p::2::_iface::close {} ::p::2::_iface::close.1 + + + #proc ::p::2::_iface::(GET)properties.1 {_ID_} { + # set ::p::2::_iface::o_properties + #} + #interp alias {} ::p::2::_iface::(GET)properties {} ::p::2::_iface::(GET)properties.1 + + #interp alias {} ::p::2::properties {} ::p::2::_iface::(GET)properties + + + #proc ::p::2::_iface::(GET)methods.1 {_ID_} { + # set ::p::2::_iface::o_methods + #} + #interp alias {} ::p::2::_iface::(GET)methods {} ::p::2::_iface::(GET)methods.1 + #interp alias {} ::p::2::methods {} ::p::2::_iface::(GET)methods + + + + + + #link from object to interface (which in this case are one and the same) + + #interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen [::p::ifaces::>2 --] + #interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed [::p::ifaces::>2 --] + #interp alias {} ::p::2::open {} ::p::2::_iface::open [::p::ifaces::>2 --] + #interp alias {} ::p::2::close {} ::p::2::_iface::close [::p::ifaces::>2 --] + + interp alias {} ::p::2::isOpen {} ::p::2::_iface::isOpen + interp alias {} ::p::2::isClosed {} ::p::2::_iface::isClosed + interp alias {} ::p::2::open {} ::p::2::_iface::open + interp alias {} ::p::2::close {} ::p::2::_iface::close + + + #namespace eval ::p::2 "namespace export $method" + + ####################################################################################### + + + + + + + set ::pattern::initialised 1 + + + ::p::internals::new_object ::p::>interface "" 3 + #create a convenience object on which to manipulate the >ifinfo interface + #set IF [::>pattern .. Create ::p::>interface] + set IF ::p::>interface + + + #!todo - put >ifinfo on a separate pStack so that end-user can more freely treat interfaces as objects? + # (or is forcing end user to add their own pStack/iStack ok .. ?) + # + ::p::>interface .. AddPatternInterface 2 ;# + + ::p::>interface .. PatternVarspace _iface + + ::p::>interface .. PatternProperty methods + ::p::>interface .. PatternPropertyRead methods {} { + varspace _iface + var {o_methods alias} + return $alias + } + ::p::>interface .. PatternProperty properties + ::p::>interface .. PatternPropertyRead properties {} { + varspace _iface + var o_properties + return $o_properties + } + ::p::>interface .. PatternProperty variables + + ::p::>interface .. PatternProperty varspaces + + ::p::>interface .. PatternProperty definition + + ::p::>interface .. Constructor {{usedbylist {}}} { + #var this + #set this @this@ + #set ns [$this .. Namespace] + #puts "-> creating ns ${ns}::_iface" + #namespace eval ${ns}::_iface {} + + varspace _iface + var o_constructor o_variables o_properties o_methods o_definition o_usedby o_varspace o_varspaces + + set o_constructor [list] + set o_variables [list] + set o_properties [dict create] + set o_methods [dict create] + set o_varspaces [list] + array set o_definition [list] + + foreach usedby $usedbylist { + set o_usedby(i$usedby) 1 + } + + + } + ::p::>interface .. PatternMethod isOpen {} { + varspace _iface + var o_open + + return $o_open + } + ::p::>interface .. PatternMethod isClosed {} { + varspace _iface + var o_open + + return [expr {!$o_open}] + } + ::p::>interface .. PatternMethod open {} { + varspace _iface + var o_open + set o_open 1 + } + ::p::>interface .. PatternMethod close {} { + varspace _iface + var o_open + set o_open 0 + } + ::p::>interface .. PatternMethod refCount {} { + varspace _iface + var o_usedby + return [array size o_usedby] + } + + set ::p::2::_iface::o_open 1 + + + + + uplevel #0 {pattern::util::package_require_min patternlib 1.2.4} + #uplevel #0 {package require patternlib} + return 1 +} + + + +proc ::p::merge_interface {old new} { + #puts stderr " ** ** ** merge_interface $old $new" + set ns_old ::p::$old + set ns_new ::p::$new + + upvar #0 ::p::${new}:: IFACE + upvar #0 ::p::${old}:: IFACEX + + if {![catch {set c_arglist $IFACEX(c,args)}]} { + #constructor + #for now.. just add newer constructor regardless of any existing one + #set IFACE(c,args) $IFACEX(c,args) + + #if {![info exists IFACE(c,args)]} { + # #target interface didn't have a constructor + # + #} else { + # # + #} + } + + + set methods [::list] + foreach nm [array names IFACEX m-1,name,*] { + lappend methods [lindex [split $nm ,] end] ;#use the method key-name not the value! (may have been overridden) + } + + #puts " *** merge interface $old -> $new ****merging-in methods: $methods " + + foreach method $methods { + if {![info exists IFACE(m-1,name,$method)]} { + #target interface doesn't yet have this method + + set THISNAME $method + + if {![string length [info command ${ns_new}::$method]]} { + + if {![set ::p::${old}::_iface::o_open]} { + #interp alias {} ${ns_new}::$method {} ${ns_old}::$method + #namespace eval $ns_new "namespace export [namespace tail $method]" + } else { + #wait to compile + } + + } else { + error "merge interface - command collision " + } + #set i 2 ??? + set i 1 + + } else { + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + + set i [incr IFACE(m-1,chain,$method)] + + set THISNAME ___system___override_${method}_$i + + #move metadata using subindices for delegated methods + set IFACE(m-$i,name,$method) $IFACE(m-1,name,$method) + set IFACE(m-$i,iface,$method) $IFACE(m-1,iface,$method) + set IFACE(mp-$i,$method) $IFACE(mp-1,$method) + + set IFACE(m-$i,body,$method) $IFACE(m-1,body,$method) + set IFACE(m-$i,args,$method) $IFACE(m-1,args,$method) + + + #set next [::p::next_script $IFID0 $method] + if {![string length [info command ${ns_new}::$THISNAME]]} { + if {![set ::p::${old}::_iface::o_open]} { + interp alias {} ${ns_new}::$THISNAME {} ${ns_old}::$method + namespace eval $ns_new "namespace export $method" + } else { + #wait for compile + } + } else { + error "merge_interface - command collision " + } + + } + + array set IFACE [::list \ + m-1,chain,$method $i \ + m-1,body,$method $IFACEX(m-1,body,$method) \ + m-1,args,$method $IFACEX(m-1,args,$method) \ + m-1,name,$method $THISNAME \ + m-1,iface,$method $old \ + ] + + } + + + + + + #array set ${ns_new}:: [array get ${ns_old}::] + + + #!todo - review + #copy everything else across.. + + foreach {nm v} [array get IFACEX] { + #puts "-.- $nm" + if {([string first "m-1," $nm] != 0) && ($nm ne "usedby")} { + set IFACE($nm) $v + } + } + + #!todo -write a test + set ::p::${new}::_iface::o_open 1 + + #!todo - is this done also when iface compiled? + #namespace eval ::p::$new {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place + + return +} + + + + +#detect attempt to treat a reference to a method as a property +proc ::p::internals::commandrefMisuse_TraceHandler {OID field args} { +#puts "commandrefMisuse_TraceHandler fired OID:$OID field:$field args:$args" + lassign [lrange $args end-2 end] vtraced vidx op + #NOTE! cannot rely on vtraced as it may have been upvared + + switch -- $op { + write { + error "$field is not a property" "property ref write failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + unset { + #!todo - monitor stat of Tcl bug# 1911919 - when/(if?) fixed - reinstate 'unset' trace + #trace add variable $traced {read write unset} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #!todo - don't use vtraced! + trace add variable $vtraced {read write unset array} [concat ::p::internals::commandrefMisuse_TraceHandler $OID $field $args] + + #pointless raising an error as "Any errors in unset traces are ignored" + #error "cannot unset. $field is a method not a property" + } + read { + error "$field is not a property (args $args)" "property ref read failure for property $field (OID: $OID refvariable: [lindex $args 0])" + } + array { + error "$field is not a property (args $args)" "property ref use as array failure for property $field (OID: $OID refvariable: [lindex $args 0])" + #error "unhandled operation in commandrefMisuse_TraceHandler - got op:$op expected read,write,unset. OID:$OID field:$field args:$args" + } + } + + return +} + + + + +#!todo - review calling-points for make_dispatcher.. probably being called unnecessarily at some points. +# +# The 'dispatcher' is an object instance's underlying object command. +# + +#proc ::p::make_dispatcher {obj ID IFID} { +# proc [string map {::> ::} $obj] {{methprop INFO} args} [string map [::list @IID@ $IFID @oid@ $ID] { +# ::p::@IID@ $methprop @oid@ {*}$args +# }] +# return +#} + + + + +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +#aliased from ::p::${OID}:: +# called when no DefaultMethod has been set for an object, but it is called with indices e.g >x something +proc ::p::internals::no_default_method {_ID_ args} { + puts stderr "p::internals::no_default_method _ID_:'$_ID_' args:'$args'" + lassign [lindex [dict get $_ID_ i this] 0] OID alias default_method object_command wrapped + tailcall error "No default method on object $object_command. (To get or set, use: $object_command .. DefaultMethod ?methodname? or use PatternDefaultMethod)" +} + +#force 1 will extend an interface even if shared. (??? why is this necessary here?) +#if IID empty string - create the interface. +proc ::p::internals::expand_interface {IID {force 0}} { + #puts stdout ">>> expand_interface $IID [info level -1]<<<" + if {![string length $IID]} { + #return [::p::internals::new_interface] ;#new interface is by default open for extending (o_open = 1) + set iid [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$iid + return $iid + } else { + if {[set ::p::${IID}::_iface::o_open]} { + #interface open for extending - shared or not! + return $IID + } + + if {[array size ::p::${IID}::_iface::o_usedby] > 1} { + #upvar #0 ::p::${IID}::_iface::o_usedby prev_usedby + + #oops.. shared interface. Copy before specialising it. + set prev_IID $IID + + #set IID [::p::internals::new_interface] + set IID [expr {$::p::ID + 1}] + ::p::>interface .. Create ::p::ifaces::>$IID + + ::p::internals::linkcopy_interface $prev_IID $IID + #assert: prev_usedby contains at least one other element. + } + + #whether copied or not - mark as open for extending. + set ::p::${IID}::_iface::o_open 1 + return $IID + } +} + +#params: old - old (shared) interface ID +# new - new interface ID +proc ::p::internals::linkcopy_interface {old new} { + #puts stderr " ** ** ** linkcopy_interface $old $new" + set ns_old ::p::${old}::_iface + set ns_new ::p::${new}::_iface + + + + foreach nsmethod [info commands ${ns_old}::*.1] { + #puts ">>> adding $nsmethod to iface $new" + set tail [namespace tail $nsmethod] + set method [string range $tail 0 end-2] ;#strip .1 + + if {![llength [info commands ${ns_new}::$method]]} { + + set oldhead [interp alias {} ${ns_old}::$method] ;#the 'head' of the cmdchain that it actually points to ie $method.$x where $x >=1 + + #link from new interface namespace to existing one. + #(we assume that since ${ns_new}::$method didn't exist, that all the $method.$x chain slots are empty too...) + #!todo? verify? + #- actual link is chainslot to chainslot + interp alias {} ${ns_new}::$method.1 {} $oldhead + + #!todo - review. Shouldn't we be linking entire chain, not just creating a single .1 pointer to the old head? + + + #chainhead pointer within new interface + interp alias {} ${ns_new}::$method {} ${ns_new}::$method.1 + + namespace eval $ns_new "namespace export $method" + + #if {[string range $method 0 4] ni {(GET) (SET) (UNSE (CONS }} { + # lappend ${ns_new}::o_methods $method + #} + } else { + if {$method eq "(VIOLATE)"} { + #ignore for now + #!todo + continue + } + + #!todo - handle how? + #error "command $cmd already exists in interface $new" + + #warning - existing chainslot will be completely shadowed by linked method. + # - existing one becomes unreachable. #!todo review!? + + + error "linkcopy_interface $old -> $new - chainslot shadowing not implemented (method $method already exists on target interface $new)" + + } + } + + + #foreach propinf [set ${ns_old}::o_properties] { + # lassign $propinf prop _default + # #interp alias {} ${ns_new}::(GET)$prop {} ::p::predator::getprop $prop + # #interp alias {} ${ns_new}::(SET)$prop {} ::p::predator::setprop $prop + # lappend ${ns_new}::o_properties $propinf + #} + + + set ${ns_new}::o_variables [set ${ns_old}::o_variables] + set ${ns_new}::o_properties [set ${ns_old}::o_properties] + set ${ns_new}::o_methods [set ${ns_old}::o_methods] + set ${ns_new}::o_constructor [set ${ns_old}::o_constructor] + + + set ::p::${old}::_iface::o_usedby(i$new) linkcopy + + + #obsolete.? + array set ::p::${new}:: [array get ::p::${old}:: ] + + + + #!todo - is this done also when iface compiled? + #namespace eval ::p::${new}::_iface {namespace ensemble create} + + + #puts stderr "copy_interface $old $new" + + #assume that the (usedby) data is now obsolete + #???why? + #set ${ns_new}::(usedby) [::list] + + #leave ::(usedby) reference in place for caller to change as appropriate - 'copy' + + return +} +################################################################################################################################################ +################################################################################################################################################ +################################################################################################################################################ + +pattern::init + +return $::pattern::version diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm index 68a14411..6fb185a9 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk-0.1.tm @@ -1,4 +1,4 @@ -#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. +#Punk - where radical modification is a craft and anti-patterns are another exploratory tool for the Pattern Punk. #Built on Tcl of course - because it's the most powerful piece of under-appreciated and alternate-thinking engineering you can plug into. @@ -6,8 +6,8 @@ namespace eval punk { proc lazyload {pkg} { package require zzzload if {[package provide $pkg] eq ""} { - zzzload::pkg_require $pkg - } + zzzload::pkg_require $pkg + } } #lazyload twapi ? @@ -50,9 +50,9 @@ namespace eval punk { } - proc ::punk::auto_execok_original name [info body ::auto_execok] + proc ::punk::auto_execok_original name [info body ::auto_execok] variable better_autoexec - + #set better_autoexec 0 ;#use this var via better_autoexec only #proc ::punk::auto_execok_windows name { # ::punk::auto_execok_original $name @@ -141,6 +141,7 @@ namespace eval punk { } if {[llength [file split $name]] != 1} { + #has a path foreach ext $execExtensions { set file ${name}${ext} if {[file exists $file] && ![file isdirectory $file]} { @@ -164,21 +165,45 @@ namespace eval punk { } foreach var {PATH Path path} { - if {[info exists env($var)]} { - append path ";$env($var)" - } + if {[info exists env($var)]} { + append path ";$env($var)" + } } #change2 - set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + if {[file extension $name] ne "" && [string tolower [file extension $name]] in [string tolower $execExtensions]} { + set lookfor [list $name] + } else { + set lookfor [lmap ext $execExtensions {string cat ${name} ${ext}}] + } + #puts "-->$lookfor" foreach dir [split $path {;}] { + set dir [string trim $dir {\\}] ;#trailing slash will result in a tail such as "/python.exe" #set dir [file normalize $dir] # Skip already checked directories if {[info exists checked($dir)] || ($dir eq "")} { continue } set checked($dir) {} - + + #surprisingly fast + #set matches [glob -nocomplain -dir $dir -types f -tails {*}$lookfor] + ##puts "--dir $dir matches:$matches" + #if {[llength $matches]} { + # set file [file join $dir [lindex $matches 0]] + # #puts "--match0:[lindex $matches 0] file:$file" + # return [set auto_execs($name) [list $file]] + #} + + #what if it's a link? + #foreach match [glob -nocomplain -dir $dir -types f -tail {*}$lookfor] { + # set file [file join $dir $match] + # if {[file exists $file]} { + # return [set auto_execs($name) [list $file]] + # } + #} + + #safest? could be a link? foreach match [glob -nocomplain -dir $dir -tail {*}$lookfor] { set file [file join $dir $match] if {[file exists $file] && ![file isdirectory $file]} { @@ -209,7 +234,7 @@ namespace eval punk { #review - what if punk package reloaded - but ::auto_execs has updated path for winget.exe? #what if we create another interp and use the same ::auto_execs? The appdir won't be detected. #TODO - see if there is a proper windows way to determine where the 'reparse point' apps are installed - + #winget is installed on all modern windows and is an example of the problem this addresses @@ -223,9 +248,9 @@ namespace eval punk { upvar ::punk::can_exec_windowsapp can_exec_windowsapp upvar ::punk::windowsappdir windowsappdir upvar ::punk::cmdexedir cmdexedir - + if {$windowsappdir eq ""} { - #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' + #we are targeting the winget location under the presumption this is where microsoft store apps are stored as 'reparse points' #Tcl (2025) can't exec when given a path to these 0KB files #This path is probably something like C:/Users/username/AppData/Local/Microsoft/WindowsApps if {!([info exists ::env(LOCALAPPDATA)] && @@ -261,13 +286,13 @@ namespace eval punk { return [file join $windowsappdir $name] } if {$cmdexedir eq ""} { - #cmd.exe very unlikely to move + #cmd.exe very unlikely to move set cmdexedir [file dirname [lindex [::punk::auto_execok_windows cmd.exe] 0]] #auto_reset never seems to exist as a command - because auto_reset deletes all commands in the ::auto_index - #anyway.. it has other side effects (affects auto_load) + #anyway.. it has other side effects (affects auto_load) } return "[file join $cmdexedir cmd.exe] /c $name" - } + } return $default_auto }] @@ -279,9 +304,9 @@ namespace eval punk { #repltelemetry cooperation with other packages such as shellrun -#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists +#Maintenance warning: shellrun expects repltelemetry_emmitters to exist if punk namespace exists namespace eval punk { - variable repltelemetry_emmitters + variable repltelemetry_emmitters #don't stomp.. even if something created this namespace in advance and is 'cooperating' a bit early if {![info exists repltelemetry_emitters]} { set repltelemetry_emmitters [list] @@ -376,7 +401,7 @@ if {![llength [info commands ::ansistring]]} { package require punk::aliascore ;#mostly punk::lib aliases punk::aliascore::init -force 1 -package require punk::repl::codethread +package require punk::repl::codethread package require punk::config #package require textblock package require punk::console ;#requires Thread @@ -385,6 +410,9 @@ package require punk::winpath ;# for windows paths - but has functions that can package require punk::repo package require punk::du package require punk::mix::base +package require base64 + +package require punk::pipe namespace eval punk { # -- --- --- @@ -415,7 +443,7 @@ namespace eval punk { package require shellfilter package require punkapp package require funcl - + package require struct::list package require fileutil #package require punk::lib @@ -435,8 +463,8 @@ namespace eval punk { #----------------------------------- # todo - load initial debug state from config debug off punk.unknown - debug level punk.unknown 1 - debug off punk.pipe + debug level punk.unknown 1 + debug off punk.pipe debug level punk.pipe 4 debug off punk.pipe.var debug level punk.pipe.var 4 @@ -478,7 +506,7 @@ namespace eval punk { uplevel 1 [list set $varname $obj2] } - interp alias "" strlen "" ::punk::strlen + interp alias "" strlen "" ::punk::strlen interp alias "" str_len "" ::punk::strlen interp alias "" objclone "" ::punk::objclone #proc ::strlen {str} { @@ -487,6 +515,7 @@ namespace eval punk { #proc ::objclone {obj} { # append obj2 $obj {} #} + #----------------------------------------------------------------------------------- #order of arguments designed for pipelining #review - 'piper_' prefix is a naming convention for functions that are ordered for tail-argument pipelining @@ -502,6 +531,351 @@ namespace eval punk { proc ::punk::K {x y} { return $x} + #todo ansigrep? e.g grep using ansistripped value + proc grepstr1 {pattern data} { + set data [string map {\r\n \n} $data] + set lines [split $data \n] + set matches [lsearch -all -regexp $lines $pattern] + set max [lindex $matches end] + set w1 [string length $max] + set result "" + set H [a+ green bold overline] + set R \x1b\[m + foreach m $matches { + set ln [lindex $lines $m] + set ln [regsub -all $pattern $ln $H&$R] + append result [format %${w1}s $m] " $ln" \n + } + set result [string trimright $result \n] + return $result + } + + #---------------------- + #todo - fix overtype + #create test + #overtype::renderline -insert_mode 0 -transparent 1 [a+ green]-----[a] " [a+ underline]x[a]" + #---------------------- + + + punk::args::define { + @id -id ::punk::grepstr + @cmd -name punk::grepstr\ + -summary\ + "Grep for regex pattern in plaintext of supplied (possibly ANSI) string."\ + -help\ + "The grepstr command can find strings in ANSI text even if there are interspersed + ANSI colour codes etc. Even if a word has different coloured/styled letters, the + regex can match the plaintext. (Search is performed on ansistripped text, and then + the matched sections are highlighted and overlayed on the original styled/colourd + input. + If the input string has ANSI movement codes - the resultant text may not be directly + searchable because the parts of a word may be separated by various codes and other + plain text. To search such an input string, the string should first be 'rendered' to + a form where the ANSI only represents SGR styling (and perhaps other non-movement + codes) using something like overtype::renderline or overtype::rendertext." + + @leaders -min 0 -max 0 + @opts + -returnlines -type string -typesynopsis matched|all -default breaksandmatches -choicecolumns 1 -choices {matched all breaksandmatches} -choicelabels { + "matched"\ + " Return only lines that matched." + "breaksandmatches"\ + " Return configured --break= lines in between non-consecutive matches" + "all"\ + " Return all lines. + This has a similar effect to the 'grep' trick of matching on 'pattern|$' + (The $ matches all lines that have an end; ie all lines, but there is no + associated character to which to apply highlighting) + except that when instead using -returnlines all with --line-number, the * + indicator after the linenumber will only be highlighted for lines with matches, + and the following matchcount will indicate zero for non-matching lines." + } + -B|--before-context= -parsekey "--before-context" -default 0 -type integer -typesynopsis num + -C|--context= -parsekey "--context" -default 0 -type integer -typesynopsis num -help\ + "Print num lines of leading and trailing context surrounding each match." + -A|--after-context= -parsekey "--after-context" -default 0 -type integer -typesynopsis num + --break= -type string -default "-- %c%\U2260" -help\ + "When returning matched lines and there is a break in consecutive output, + display the break with the given string. %c% is a placeholder for the + number of lines skipped. + Use empty-string for an empty line as a break display. + grepstr --break= needle $haystacklines + + The unix grep utility commonly uses -- for this indicator. + grepstr --break=-- needle $haystacklines + + Customisation example: + grepstr -n \"--break=(skipped %c% lines)\" needle $haystacklines + " + -ansistrip -type none -help\ + "Strip all ansi codes from the input string before processing. + This is not necessary for regex matching purposes, as the matching is always + performed on the ansistripped characters anyway, but by stripping ANSI, the + result only has the ANSI supplied by the -highlight option." + + #-n|--line-number as per grep utility, except that we include a * for matches + -n|--line-number -type none -help\ + "Each output line is preceded by its relative line number in the file, starting at line 1. + For lines that matched the regex, the line number will be suffixed with a * indicator + with the same highlighting as the matched string(s). + The number of matches in the line immediately follows the * + For lines with no matches the * indicator is present with no highlighting and suffixed + with zeros." + -i|--ignore-case -type none -help\ + "Perform case insensitive matching." + -highlight -type list -typesynopsis ansinames -default {green bold Black underline overline} -help\ + "list of ANSI SGR style codes as supported by and documented in punk::ansi::a?" + -- -type none + @values + pattern -type string -help\ + "regex pattern to match in plaintext portion of ANSI string" + string -type string + } + proc grepstr {args} { + lassign [dict values [punk::args::parse $args withid ::punk::grepstr]] leaders opts values received + set pattern [dict get $values pattern] + set data [dict get $values string] + set do_strip 0 + if {[dict exists $received -ansistrip]} { + set data [punk::ansi::ansistrip $data] + } + set highlight [dict get $opts -highlight] + set opt_returnlines [dict get $opts -returnlines] + set context [dict get $opts --context] ;#int + set beforecontext [dict get $opts --before-context] + set beforecontext [expr {max($beforecontext,$context)}] + set aftercontext [dict get $opts --after-context] + set aftercontext [expr {max($aftercontext,$context)}] + set break [dict get $opts --break] + set ignorecase [dict exists $received --ignore-case] + if {$ignorecase} { + set nocase "-nocase" + } else { + set nocase "" + } + + + if {[dict exists $received --line-number]} { + set do_linenums 1 ;#display lineindex+1 + } else { + set do_linenums 0 + } + + if {[llength $highlight] == 0} { + set H "" + set R "" + } else { + set H [a+ {*}$highlight] + set R \x1b\[m + } + + set data [string map {\r\n \n} $data] + if {![punk::ansi::ta::detect $data]} { + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $lines $pattern] + set result "" + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + #matches|breaksandmatches + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + incr max + } + set w1 [string length $max] + #lineindex is zero based - display of linenums is 1 based + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set ln [regsub -all {*}$nocase -- $pattern $ln $H&$R] ;#n + set matchcount [regexp -all {*}$nocase -- $pattern $ln] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + } else { + if {$do_linenums} { + append col1 "*000" + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + if {$do_linenums} { + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + + } + } else { + set plain [punk::ansi::ansistrip $data] + set plainlines [split $plain \n] + set lines [split $data \n] + set matches [lsearch -all {*}$nocase -regexp $plainlines $pattern] + if {$opt_returnlines eq "all"} { + set returnlines [punk::lib::range 0 [llength $lines]-1] + } else { + set returnlines $matches + } + set max [lindex $returnlines end] + if {[string is integer -strict $max]} { + #if max index is 9 - linenum will be 10, (99->100 etc) - so add one in case we're on such a boundary. + incr max + } + set w1 [string length $max] + set result "" + set placeholder \UFFEF ;#review + set resultlines [dict create] + foreach lineindex $returnlines { + set ln [lindex $lines $lineindex] + set col1 "" + if {$do_linenums} { + set col1 [format "%${w1}s " [expr {$lineindex+1}]] + } + if {$lineindex in $matches} { + set plain_ln [lindex $plainlines $lineindex] + set parts [regexp -all {*}$nocase -indices -inline -- $pattern $plain_ln] + set matchcount [llength $parts] + if {$do_linenums} { + append col1 $H*$R[format %03s $matchcount] + } + if {[llength $parts] == 0} { + #This probably can't happen (?) + #If it does.. it's more likely to be an issue with our line index than with regexp + puts stderr "Unexpected regex mismatch in grepstr - line marked with ??? (shouldn't happen)" + set matchshow "??? $ln" + #dict set resultlines $lineindex $show + } else { + set overlay "" + set i 0 + foreach prange $parts { + lassign $prange s e + set prelen [expr {$s - $i}] + append overlay [string repeat $placeholder $prelen] $H[string range $plain_ln $s $e]$R + set i [expr {$e + 1}] + } + set tail [string range $plain_ln $e+1 end] + append overlay [string repeat $placeholder [string length $tail]] + #puts "$overlay" + #puts "$ln" + set rendered [overtype::renderline -transparent $placeholder -insert_mode 0 $ln $overlay] + if {$do_linenums} { + set matchshow "$col1 $rendered" + } else { + set matchshow $rendered + } + } + #--------------------------------------------------------------- + set prelines [lrange $lines $lineindex-$beforecontext $lineindex-1] + set s [expr {$lineindex-$beforecontext-1}] + if {$s < -1} {set s -1} + foreach p $prelines { + incr s + #append result "[format %${w1}s [expr {$s+1}]]- " " " $p \n + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + dict set resultlines $lineindex $matchshow + #--------------------------------------------------------------- + set postlines [lrange $lines $lineindex+1 $lineindex+$aftercontext] + set s $lineindex + foreach p $postlines { + incr s + if {![dict exists $resultlines $s]} { + if {$do_linenums} { + set show "[format "%${w1}s " [expr {$s+1}]]- $p" + } else { + set show $p + } + dict set resultlines $s $show + } + } + #--------------------------------------------------------------- + } else { + if {$do_linenums} { + append col1 "*000" + set show "$col1 $ln" + } else { + set show $ln + } + dict set resultlines $lineindex $show + } + } + } + set ordered_resultlines [lsort -integer [dict keys $resultlines]] + set result "" + set i -1 + set do_break 0 + if {$opt_returnlines eq "breaksandmatches"} { + set do_break 1 + } + if {$do_break} { + foreach r $ordered_resultlines { + incr i + if {$r > $i} { + set c [expr {$r - $i}] + append result [string map [list %c% $c] $break] \n + } + append result [dict get $resultlines $r] \n + set i $r + } + if {$i<[llength $lines]-1} { + set c [expr {[llength $lines]-1-$i}] + append result [string map [list %c% $c] $break] \n + } + } else { + foreach r $ordered_resultlines { + append result [dict get $resultlines $r] \n + } + } + set result [string trimright $result \n] + return $result + } + proc stacktrace {} { set stack "Stack trace:\n" for {set i 1} {$i < [info level]} {incr i} { @@ -563,22 +937,24 @@ namespace eval punk { #get last command result that was run through the repl proc ::punk::get_runchunk {args} { - set argd [punk::args::get_dict { + set argd [punk::args::parse $args withdef { @id -id ::punk::get_runchunk + @cmd -name "punk::get_runchunk" -help\ + "experimental" @opts - -1 -optional 1 -type none - -2 -optional 1 -type none + -1 -optional 1 -type none + -2 -optional 1 -type none @values -min 0 -max 0 - } $args] + }] #todo - make this command run without truncating previous runchunks set runchunks [tsv::array names repl runchunks-*] set sortlist [list] foreach cname $runchunks { set num [lindex [split $cname -] 1] - lappend sortlist [list $num $cname] + lappend sortlist [list $num $cname] } - set sorted [lsort -index 0 -integer $sortlist] + set sorted [lsort -index 0 -integer $sortlist] set chunkname [lindex $sorted end-1 1] set runlist [tsv::get repl $chunkname] #puts stderr "--$runlist" @@ -635,10 +1011,10 @@ namespace eval punk { set inopts 1 } else { #leave loop at first nonoption - i should be index of file - break + break } } else { - #leave for next iteration to check + #leave for next iteration to check set inopts 0 } incr i @@ -654,44 +1030,8 @@ namespace eval punk { set ::argc $argc return -code $code $return } - #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ - # - #we can't provide a float comparison suitable for every situation, - #but we pick something reasonable, keep it stable, and document it. - proc float_almost_equal {a b} { - package require math::constants - set diff [expr {abs($a - $b)}] - if {$diff <= $math::constants::eps} { - return 1 - } - set A [expr {abs($a)}] - set B [expr {abs($b)}] - set largest [expr {($B > $A) ? $B : $A}] - return [expr {$diff <= $largest * $math::constants::eps}] - } - #boolean could be tr, true, y, ye, yes,Yes, 1 , 0 etc. - proc boolean_equal {a b} { - #equivalenttly xnor: expr {!(($a && 1) ^ ($b && 1))} ;# less clear and no discernable timing benefit. - expr {($a && 1) == ($b && 1)} - } - #debatable whether boolean_almost_equal is likely to be surprising or helpful. - #values from a calculation that are extremely close to zero but aren't false could also be surprising - especially if they compare equal numerically - #perhaps a fuzzy-boolean is a step too far for a default - but it's inline with float-comparison for pattern-matching. use an even more complex classifier? (^&~) ? - proc boolean_almost_equal {a b} { - if {[string is double -strict $a]} { - if {[float_almost_equal $a 0]} { - set a 0 - } - } - if {[string is double -strict $b]} { - if {[float_almost_equal $b 0]} { - set b 0 - } - } - #must handle true,no etc. - expr {($a && 1) == ($b && 1)} - } + proc varinfo {vname {flag ""}} { @@ -703,9 +1043,9 @@ namespace eval punk { error "can't read \"$vname\": no such variable" } set inf [shellfilter::list_element_info [list $v]] - set inf [dict get $inf 0] + set inf [dict get $inf 0] if {$flag eq "-v"} { - return $inf + return $inf } set output [dict create] @@ -781,7 +1121,7 @@ namespace eval punk { } else { append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -791,162 +1131,12 @@ namespace eval punk { } return $varlist } - proc splitstrposn {s p} { - if {$p <= 0} { - if {$p == 0} { - list "" $s - } else { - list $s "" - } - } else { - scan $s %${p}s%s - } - } - proc splitstrposn_nonzero {s p} { - scan $s %${p}s%s - } - #split top level of patterns only. - proc _split_patterns_memoized {varspecs} { - set name_mapped [pipecmd_namemapping $varspecs] - set cmdname ::punk::pipecmds::split_patterns::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - set result [_split_patterns $varspecs] - proc $cmdname {} [list return $result] - #debug.punk.pipe.compile {proc $cmdname} 4 - return $result - } - proc _split_patterns {varspecs} { - - set varlist [list] - # @ @@ - list and dict functions - # / level separator - # # list count, ## dict size - # % string functions - # ! not - set var_terminals [list "@" "/" "#" "%" "!" ">" "<"] ;# (> required for insertionspecs at rhs of = & .= ) - #right bracket ) also ends a var - but is different depending on whether var is array or basic. For array - it forms part of the varname - - #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# - #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string - #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' - set in_brackets 0 ;#count depth - set in_atom 0 - #set varspecs [string trimleft $varspecs ,] - set token "" - #if {[string first "," $varspecs] <0} { - # return $varspecs - #} - set first_term -1 - set token_index 0 ;#index of terminal char within each token - set indq 0 - set inesc 0 ;#whether last char was backslash (see also punk::escv) - set prevc "" - set char_index 0 - foreach c [split $varspecs ""] { - if {$indq} { - if {$inesc} { - #puts stderr "inesc adding '$c'" - append token $c - } else { - if {$c eq {"}} { - set indq 0 - } else { - append token $c - } - } - } elseif {$in_atom} { - #ignore dquotes/brackets in atoms - pass through - append token $c - #set nextc [lindex $chars $char_index+1] - if {$c eq "'"} { - set in_atom 0 - } - } elseif {$in_brackets > 0} { - append token $c - if {$c eq ")"} { - incr in_brackets -1 - } - } else { - if {$c eq {"} && !$inesc} { - set indq 1 - } elseif {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - set token "" - set token_index -1 ;#reduce by 1 because , not included in next token - set first_term -1 - } else { - append token $c - switch -exact -- $c { - ' { - set in_atom 1 - } - ( { - incr in_brackets - } - default { - if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index - } - } - } - } - } - set prevc $c - if {$c eq "\\"} { - #review - if {$inesc} { - set inesc 0 - } else { - set token [string range $token 0 end-1] - set inesc 1 - } - } else { - set inesc 0 - } - incr token_index - incr char_index - } - if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - #lassign [scan $token %${first_term}s%s] var spec - set var [string range $token 0 $first_term-1] - set spec [string range $token $first_term end] ;#key section includes the terminal char which ended the var and starts the spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list [string trim $var] [string trim $spec]] - } - return $varlist - } proc _split_var_key_at_unbracketed_comma {varspecs} { set varlist [list] set var_terminals [list "@" "/" "#" "!"] #except when prefixed directly by pin classifier ^ - set protect_terminals [list "^"] ;# e.g sequence ^# + set protect_terminals [list "^"] ;# e.g sequence ^# #also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string #ie the one usecase is '/n to match either empty string or missing item at position n. For this one usecase - we miss the capability to atom match paths/urls .. '/usr/local/et' set in_brackets 0 @@ -966,27 +1156,17 @@ namespace eval punk { } } else { if {$c eq ","} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] + set token "" set token_index -1 ;#reduce by 1 because , not included in next token set first_term -1 } else { append token $c if {$first_term == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { - set first_term $token_index + set first_term $token_index } elseif {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -994,18 +1174,7 @@ namespace eval punk { incr token_index } if {[string length $token]} { - #lappend varlist [splitstrposn $token $first_term] - set var $token - set spec "" - if {$first_term > 0} { - lassign [scan $token %${first_term}s%s] var spec - } else { - if {$first_term == 0} { - set var "" - set spec $token - } - } - lappend varlist [list $var $spec] + lappend varlist [punk::lib::string_splitbefore $token $first_term] } return $varlist } @@ -1029,6 +1198,7 @@ namespace eval punk { } else { if {$c eq ","} { if {$first_term > -1} { + #lassign [punk::lib::string_splitbefore $token $first_term] v k set v [string range $token 0 $first_term-1] set k [string range $token $first_term end] ;#key section includes the terminal char lappend varlist [list $v $k] @@ -1041,12 +1211,12 @@ namespace eval punk { } else { if {$first_term == -1} { if {$c in $var_terminals} { - set first_term $token_index + set first_term $token_index } } append token $c if {$c eq "("} { - set in_brackets 1 + set in_brackets 1 } } } @@ -1067,7 +1237,7 @@ namespace eval punk { proc fp_restructure {selector data} { if {$selector eq ""} { fun=.= {val $input} 0 || abs($offset) >= $len)} { set action ?mismatch-list-index-out-of-range break @@ -1424,7 +1594,7 @@ namespace eval punk { } elseif {$start eq "end"} { #ok } elseif {$do_bounds_check} { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0 || abs($startoffset) >= $len} { set action ?mismatch-list-index-out-of-range @@ -1481,7 +1651,7 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + } else { #keyword 'pipesyntax' at beginning of error message error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] @@ -1513,23 +1683,40 @@ namespace eval punk { return [dict create -assigned $leveldata -action $action -lhs $lhs -rhs $rhs] } - #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script + #todo - fp_destructure - return a function-pipeline that can then be transformed to a funcl and finally a more efficient tcl script proc destructure_func {selector data} { #puts stderr ".d." set selector [string trim $selector /] - #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position - #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position - - #map some problematic things out of the way in a manner that maintains some transparency - #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} - #The selector forms part of the proc name - set selector_safe [string map [list ? * {$} "" "\x1b\[" "\x1b\]" {[} {]} :: {;} " " \t \n \r ] $selector] + #upvar v_list_idx v_list_idx ;#positional tracker for /@ - list position + #upvar v_dict_idx v_dict_idx ;#positional tracker for /@@ - dict position + + #map some problematic things out of the way in a manner that maintains some transparency + #e.g glob chars ? * in a command name can make testing using {[info commands $cmd] ne ""} produce spurious results - requiring a stricter (and slower) test such as {$cmd in [info commands $cmd]} + #The selector forms part of the proc name + #review - compare with pipecmd_namemapping + set selector_safe [string map [list\ + ? \ + * \ + \\ \ + {"} \ + {$} \ + "\x1b\[" \ + "\x1b\]" \ + {[} \ + {]} \ + :: \ + {;} \ + " " \ + \t \ + \n \ + \r \ + ] $selector] set cmdname ::punk::pipecmds::destructure::_$selector_safe if {[info commands $cmdname] ne ""} { return [$cmdname $data] ;# note upvar 2 for stateful v_list_idx to be resolved in _multi_bind_result context } - + set leveldata $data set body [destructure_func_build_procbody $cmdname $selector $data] @@ -1553,8 +1740,8 @@ namespace eval punk { proc destructure_func_build_procbody {cmdname selector data} { set script "" #place selector in comment in script only - if there is an error in selector we pick it up when building the script. - #The script itself should only be returning errors in its action key of the result dictionary - append script \n [string map [list $selector] {# set selector {}}] + #The script itself should only be returning errors in its action key of the result dictionary + append script \n [string map [list $selector] {# set selector {}}] set subindices [split $selector /] append script \n [string map [list [list $subindices]] {# set subindices }] set action ?match ;#default assumption. Alternatively set to ?mismatch or ?mismatch- and always break @@ -1562,7 +1749,7 @@ namespace eval punk { #append script \n {set assigned ""} ;#review set active_key_type "" append script \n {# set active_key_type ""} - set lhs "" + set lhs "" #append script \n [tstr {set lhs ${{$lhs}}}] append script \n {set lhs ""} set rhs "" @@ -1582,9 +1769,9 @@ namespace eval punk { #dict 'index' when using stateful @@ etc to iterate over dict instead of by key set tpl_return_mismatch_dict_index_out_of_range {return [dict create -assigned $leveldata -action ?mismatch-dict-index-out-of-range -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} set tpl_return_mismatch_dict_key_not_found {return [dict create -assigned $leveldata -action ?mismatch-dict-key-not-found -lhs $lhs -rhs xxx -index_operations {${$INDEX_OPERATIONS}}]} - - if {![string length $selector]} { + + if {![string length $selector]} { #just return $leveldata set script { dict create -assigned $leveldata -action ?match -lhs "" -rhs $leveldata @@ -1598,7 +1785,7 @@ namespace eval punk { #pure numeric keylist - put straight to lindex # #NOTE: this direct access e.g v/0/1/2 doesn't check out of bounds which is at odds with list access containing @ - #We will leave this as a syntax for different (more performant) behaviour + #We will leave this as a syntax for different (more performant) behaviour #- it's potentially a little confusing - but it would be a shame not to have the possibility to take advantage of the lindex deep indexing capability in pattern matching. #TODO - review and/or document # @@ -1625,7 +1812,7 @@ namespace eval punk { # -- --- --- } if {[string match @@* $selector]} { - #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc + #part following a double @ is dict key possibly with forward-slash separators for subpath access e.g @@key/subkey/etc set rawkeylist [split $selector /] ;#first key retains @@ - may be just '@@' set keypath [string range $selector 2 end] set keylist [split $keypath /] @@ -1659,11 +1846,11 @@ namespace eval punk { foreach index $subindices { #set index_operation "unspecified" set level_script_complete 0 ;#instead of break - as we can't use data to determine break when building script - set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] + set SUBPATH [join [lrange $subindices 0 $i_keyindex] /] append script \n "# ------- START index:$index subpath:$SUBPATH ------" set lhs $index - append script \n "set lhs $index" - + append script \n "set lhs {$index}" + set assigned "" append script \n {set assigned ""} @@ -1677,21 +1864,21 @@ namespace eval punk { # do_bounds_check shouldn't need to be in script set do_bounds_check 0 ;#modified by leading single @ for list operations - doesn't apply to certain items like 'head','tail' which have specifically defined bounds-checks implicit in their normal meaning. - #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. + #thse have anyhead and anytail for explicit allowance to be used on lists with insufficient items to produce values. #append script \n {set do_boundscheck 0} switch -exact -- $index { # - @# { #list length set active_key_type "list" if {$get_not} { - lappend INDEX_OPERATIONS not-list + lappend INDEX_OPERATIONS not-list append script \n {# set active_key_type "list" index_operation: not-list} append script \n { if {[catch {llength $leveldata}]} { - #not a list - not-length is true + #not a list - not-length is true set assigned 1 } else { - #is a list - not-length is false + #is a list - not-length is false set assigned 0 } } @@ -1710,7 +1897,7 @@ namespace eval punk { #dict size set active_key_type "dict" if {$get_not} { - lappend INDEX_OPERATIONS not-dict + lappend INDEX_OPERATIONS not-dict append script \n {# set active_key_type "dict" index_operation: not-dict} append script \n { if {[catch {dict size $leveldata}]} { @@ -1733,10 +1920,10 @@ namespace eval punk { } %# { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS string-length append script \n {# set active_key_type "" index_operation: string-length} append script \n {set assigned [string length $leveldata]} @@ -1745,10 +1932,10 @@ namespace eval punk { %%# { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%%# not string length is not supported" } - #string length - REVIEW - + #string length - REVIEW - lappend INDEX_OPERATIONS ansistring-length append script \n {# set active_key_type "" index_operation: ansistring-length} append script \n {set assigned [ansistring length $leveldata]} @@ -1756,7 +1943,7 @@ namespace eval punk { } %str { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%str - not string-get is not supported" } lappend INDEX_OPERATIONS string-get @@ -1767,7 +1954,7 @@ namespace eval punk { %sp { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%sp - not string-space is not supported" } lappend INDEX_OPERATIONS string-space @@ -1778,7 +1965,7 @@ namespace eval punk { %empty { #experimental set active_key_type "string" - if $get_not { + if {$get_not} { error "!%empty - not string-empty is not supported" } lappend INDEX_OPERATIONS string-empty @@ -1788,10 +1975,10 @@ namespace eval punk { } @words { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%words - not list-words-from-string is not supported" } - lappend INDEX_OPERATIONS list-words-from-string + lappend INDEX_OPERATIONS list-words-from-string append script \n {# set active_key_type "" index_operation: list-words-from-string} append script \n {set assigned [regexp -inline -all {\S+} $leveldata]} set level_script_complete 1 @@ -1800,10 +1987,10 @@ namespace eval punk { #experimental - leading character based on result not input(?) #input type is string - but output is list set active_key_type "list" - if $get_not { + if {$get_not} { error "!%chars - not list-chars-from-string is not supported" } - lappend INDEX_OPERATIONS list-from_chars + lappend INDEX_OPERATIONS list-from_chars append script \n {# set active_key_type "" index_operation: list-chars-from-string} append script \n {set assigned [split $leveldata ""]} set level_script_complete 1 @@ -1812,7 +1999,7 @@ namespace eval punk { #experimental - flatten one level of list #join without arg - output is list set active_key_type "string" - if $get_not { + if {$get_not} { error "!@join - not list-join-list is not supported" } lappend INDEX_OPERATIONS list-join-list @@ -1824,7 +2011,7 @@ namespace eval punk { #experimental #input type is list - but output is string set active_key_type "string" - if $get_not { + if {$get_not} { error "!%join - not string-join-list is not supported" } lappend INDEX_OPERATIONS string-join-list @@ -1834,7 +2021,7 @@ namespace eval punk { } %ansiview { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiview is not supported" } lappend INDEX_OPERATIONS string-ansiview @@ -1844,7 +2031,7 @@ namespace eval punk { } %ansiviewstyle { set active_key_type "string" - if $get_not { + if {$get_not} { error "!%# not string-ansiviewstyle is not supported" } lappend INDEX_OPERATIONS string-ansiviewstyle @@ -1855,23 +2042,23 @@ namespace eval punk { @ { #as this is a stateful list next index operation - we use not (!@) to mean there is no element at the next index (instead of returning the complement ie all elements except next) #This is in contrast to other not operations on indices e.g /!2 which returns all elements except that at index 2 - + #append script \n {puts stderr [uplevel 1 [list info vars]]} #NOTE: #v_list_idx in context of _multi_bind_result - #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) + #we call destructure_func from _mult_bind_result which in turn calls the proc (or the script on first run) append script \n {upvar 2 v_list_idx v_list_idx} set active_key_type "list" append script \n {# set active_key_type "list" index_operation: list-get-next} #e.g @1/1/@/1 the lone @ is a positional spec for this specific subkey #no normalization done - ie @2/@ will not be considered same subkey as @end/@ or @end-0/@ even if llength = 3 - #while x@,y@.= is reasonably handy - especially for args e.g $keyglob] { # set active_key_type "dict" index_operation: globkey-get-pairs-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict remove $leveldata {*}$matched] }] @@ -2285,7 +2473,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-pairs append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operations: globkey-get-pairs - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict create] foreach m $matched { dict set assigned $m [dict get $leveldata $m] @@ -2307,7 +2495,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys-not append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys-not - set matched [dict keys $leveldata ] + set matched [dict keys $leveldata {}] set assigned [dict keys [dict remove $leveldata {*}$matched]] }] @@ -2315,7 +2503,7 @@ namespace eval punk { lappend INDEX_OPERATIONS globkey-get-keys append script \n [string map [list $keyglob] { # set active_key_type "dict" index_operation: globkey-get-keys - set assigned [dict keys $leveldata ] + set assigned [dict keys $leveldata {}] }] } set level_script_complete 1 @@ -2323,7 +2511,7 @@ namespace eval punk { {@k\*@*} - {@K\*@*} { #dict value glob - return keys set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2331,22 +2519,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-keys-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match "" $v]} { + if {![string match {} $v]} { lappend assigned $k } } }] } else { lappend INDEX_OPERATIONS globvalue-get-keys - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-keys set assigned [list] tcl::dict::for {k v} $leveldata { - if {[string match "" $v]} { + if {[string match {} $v]} { lappend assigned $k } } @@ -2357,7 +2545,7 @@ namespace eval punk { {@.\*@*} { #dict value glob - return pairs set active_key_type "dict" - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2365,22 +2553,22 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-pairs-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { dict set assigned $k $v } } }] } else { lappend INDEX_OPERATIONS globvalue-get-pairs - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" index_operation: globvalue-get-pairs set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $v]} { + if {[string match {} $v]} { dict set assigned $k $v } } @@ -2389,9 +2577,9 @@ namespace eval punk { set level_script_complete 1 } {@V\*@*} - {@v\*@*} { - #dict value glob - return values + #dict value glob - return values set active_key_type dict - set keyglob [string range $index 4 end] + set valglob [string range $index 4 end] append script [tstr -return string -allowcommands { if {[catch {dict size $leveldata}]} { ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2399,11 +2587,11 @@ namespace eval punk { }] if {$get_not} { lappend INDEX_OPERATIONS globvalue-get-values-not - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;# index_operation: globvalue-get-values-not set assigned [list] tcl::dict::for {k v} $leveldata { - if {![string match $v]} { + if {![string match {} $v]} { lappend assigned $v } } @@ -2411,9 +2599,9 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS globvalue-get-values - append script \n [string map [list $keyglob] { + append script \n [string map [list $valglob] { # set active_key_type "dict" ;#index_operation: globvalue-get-value - set assigned [dict values $leveldata ] + set assigned [dict values $leveldata ] }] } set level_script_complete 1 @@ -2437,14 +2625,14 @@ namespace eval punk { # set active_key_type "dict" ;# index_operation: globkeyvalue-get-pairs-not set assigned [dict create] tcl::dict::for {k v} $leveldata { - if {[string match $k] || [string match $v]} { + if {[string match {} $k] || [string match {} $v]} { dict set assigned $k $v } } }] } - - error "globkeyvalue-get-pairs todo" + set level_script_complete 1 + puts stderr "globkeyvalue-get-pairs review" } @* { set active_key_type "list" @@ -2483,7 +2671,7 @@ namespace eval punk { append listmsg "Use var@@key to treat value as a dict and retrieve element at key" #append script \n [string map [list $listmsg] {set listmsg ""}] - + #we can't just set 'assigned' for a position spec for in/ni (not-in) because we don't have the value here to test against @@ -2544,7 +2732,7 @@ namespace eval punk { ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { #alias for 0 - for h@head,t@tail= similar to erlang/elixir hd() tl() or [head | tail] = list syntax - ${$assignment_script} + ${$assignment_script} } }] } @@ -2568,7 +2756,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range_empty]} } else { - ${$assignment_script} + ${$assignment_script} } }] } else { @@ -2577,13 +2765,13 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assignment_script} + ${$assignment_script} } }] } } tail { - #NOTE: /@tail and /tail both do bounds check. This is intentional. + #NOTE: /@tail and /tail both do bounds check. This is intentional. # #tail is a little different in that we allow tail on a single element list - returning an empty result - but it can't be called on an empty list #arguably tail could be considered as an index-out-of-range for less than 2 elements - but this would be less useful, and surprising to those coming from other pattern-matching systems. @@ -2596,7 +2784,7 @@ namespace eval punk { append script \n "# index_operation listindex-tail" \n lappend INDEX_OPERATIONS listindex-tail set assignment_script {set assigned [lrange $leveldata 1 end] ;#return zero or more elements - but only if there is something (a head) at position zero} - } + } append script \n [tstr -return string -allowcommands { if {[catch {llength $leveldata} len]} { #set action ?mismatch-not-a-list @@ -2693,7 +2881,7 @@ namespace eval punk { } raw { #get_not - return nothing?? - #no list checking.. + #no list checking.. if {$get_not} { lappend INDEX_OPERATIONS getraw-not append script \n {set assigned {}} @@ -2748,7 +2936,7 @@ namespace eval punk { } else { lappend INDEX_OPERATIONS list-getpairs } - append script \n [tstr -return string -allowcommands { + append script \n [tstr -return string -allowcommands { if {[catch {dict size $leveldata} dsize]} { #set action ?mismatch-not-a-dict ${[tstr -ret string $tpl_return_mismatch_not_a_dict]} @@ -2776,7 +2964,7 @@ namespace eval punk { if {[catch {llength $leveldata} len]} { ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } elseif {[string is integer -strict $index]} { @@ -2816,7 +3004,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2847,7 +3035,7 @@ namespace eval punk { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} } else { - ${$assign_script} + ${$assign_script} } } }] @@ -2857,7 +3045,7 @@ namespace eval punk { #set action ?mismatch-not-a-list ${[tstr -ret string $tpl_return_mismatch_not_a_list]} } else { - ${$assign_script} + ${$assign_script} } }] } @@ -2896,15 +3084,15 @@ namespace eval punk { } elseif {$start eq "end"} { #noop } else { - set startoffset [string range $start 3 end] ;#include the - from end- + set startoffset [string range $start 3 end] ;#include the - from end- set startoffset [expr $startoffset] ;#don't brace! if {$startoffset > 0} { #e.g end+1 error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } - append script \n [tstr -return string -allowcommands { - set startoffset ${$startoffset} + append script \n [tstr -return string -allowcommands { + set startoffset ${$startoffset} if {abs($startoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2916,7 +3104,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end_lessthanzero_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set end ${$end} + set end ${$end} if {$end+1 > $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -2932,7 +3120,7 @@ namespace eval punk { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector end+x_out_of_bounds_for_all_data_while_bounds_check_on] } append script \n [tstr -return string -allowcommands { - set endoffset ${$endoffset} + set endoffset ${$endoffset} if {abs($endoffset) >= $len} { #set action ?mismatch-list-index-out-of-range ${[tstr -ret string $tpl_return_mismatch_list_index_out_of_range]} @@ -3014,13 +3202,13 @@ namespace eval punk { } else { error $listmsg "destructure $selector" [list pipesyntax destructure selector $selector] } - + append script \n [string map [list $assign_script] { if {![string match ?mismatch-* $action]} { } }] - + } else { #keyword 'pipesyntax' at beginning of error message #pipesyntax error - no need to even build script - can fail now @@ -3072,7 +3260,7 @@ namespace eval punk { #dict remove can accept non-existent keys.. review do we require not-@?@key to get silence? append script \n [tstr -return string { set assigned [dict remove $leveldata ${$index}] - }] + }] } else { append script \n [tstr -return string -allowcommands { # set active_key_type "dict" @@ -3096,7 +3284,7 @@ namespace eval punk { } incr i_keyindex append script \n "# ------- END index $index ------" - } ;# end foreach + } ;# end foreach @@ -3109,157 +3297,6 @@ namespace eval punk { return $script } - #todo - recurse into bracketed sub parts - #JMN3 - #e.g @*/(x@0,y@2) - proc _var_classify {multivar} { - set cmdname ::punk::pipecmds::var_classify::_[pipecmd_namemapping $multivar] - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - - #comma seems a natural choice to split varspecs, - #but also for list and dict subelement access - #/ normally indicates some sort of hierarchical separation - (e.g in filesytems) - #so / will indicate subelements e.g @0/1 for lindex $list 0 1 - #set valsource_key_list [_split_var_key_at_unbracketed_comma $multivar] - set valsource_key_list [_split_patterns_memoized $multivar] - - - - #mutually exclusive - atom/pin - #set map [list "" ' ^ &] ;#0 = default/var/not-yet-determined 1 = atom 2 = pin - #set var_class [lmap var $valsource_key_list {expr {([set m [lsearch $map [string index [lindex $var 0] 0]]] >= 0) ? [list $var $m] : [list $var 0]}}] - #0 - novar - #1 - atom ' - #2 - pin ^ - #3 - boolean & - #4 - integer - #5 - double - #6 - var - #7 - glob (no classifier and contains * or ?) - #8 - numeric - #9 - > (+) - #10 - < (-) - - set var_names [list] - set var_class [list] - set varspecs_trimmed [list] ;#raw varspecs without pin/atom modifiers - or empty string for glob - - - set leading_classifiers [list "'" "&" "^" ] - set trailing_classifiers [list + -] - set possible_number_start [list - + . 0 1 2 3 4 5 6 7 8 9 > <] - - foreach v_key $valsource_key_list { - lassign $v_key v key - set vname $v ;#default - set classes [list] - if {$v eq ""} { - lappend var_class [list $v_key 0] - lappend varspecs_trimmed $v_key - } else { - set lastchar [string index $v end] - switch -- $lastchar { - + { - lappend classes 9 - set vname [string range $v 0 end-1] - } - - { - lappend classes 10 - set vname [string range $v 0 end-1] - } - } - set firstchar [string index $v 0] - switch -- $firstchar { - ' { - lappend var_class [list $v_key 1] - #set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - ^ { - lappend classes [list 2] - #use vname - may already have trailing +/- stripped - set vname [string range $vname 1 end] - set secondclassifier [string index $v 1] - switch -- $secondclassifier { - "&" { - #pinned boolean - lappend classes 3 - set vname [string range $v 2 end] - } - "#" { - #pinned numeric comparison instead of string comparison - #e.g set x 2 - # this should match: ^#x.= list 2.0 - lappend classes 8 - set vname [string range $vname 1 end] - } - "*" { - #pinned glob - lappend classes 7 - set vname [string range $v 2 end] - } - } - #todo - check for second tag - & for pinned boolean? - #consider requiring ^# for numeric comparisons. currently no way to do a strictly string comparison on pinned variables.... default ^var really shouldn't be doing any magic. - #while we're at it.. pinned glob would be nice. ^* - #maybe even pinned scan ^% ? regex? ^/ or ^? these would be hard to have corresponding literals in the pattern mini-lang. - #These all limit the range of varnames permissible - which is no big deal. - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed [list $vname $key] - } - & { - #we require boolean literals to be single-quoted so we can use cross-binding on boolean vars. - #ie &true is the variable true whereas &'true' or &'1' &'t' etc are literal booleans - #allow exception of &1 &0 to be literal booleans - because we disallow 0 & 1 as varnames in other contexts anyway - so it would be more consistent not to treat as varnames here. - lappend var_class [list $v_key 3] - set vname [string range $v 1 end] - lappend varspecs_trimmed [list $vname $key] - } - default { - if {([string first ? $v]) >=0 || ([string first * $v] >=0)} { - lappend var_class [list $v_key 7] ;#glob - #leave vname as the full glob - lappend varspecs_trimmed [list "" $key] - } else { - #scan vname not v - will either be same as v - or possibly stripped of trailing +/- - set numtestv [join [scan $vname %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, decimal points and sci notation - fails to handle leading dot e.g .5 - #leading . still need to test directly for double - if {[string is double -strict $vname] || [string is double -strict $numtestv]} { - if {[string is integer -strict $numtestv]} { - #this will pick up boolean 1 or 0 - but that's ok - they require "&" marker if boolean comparison desired - #integer test before double.. - #note there is also string is wide (string is wideinteger) for larger ints.. - lappend classes 4 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } else { - #double - #sci notation 1e123 etc - #also large numbers like 1000000000 - even without decimal point - (tcl bignum) - lappend classes 5 - lappend var_class [list $v_key $classes] - lappend varspecs_trimmed $v_key - } - } else { - lappend var_class [list $v_key 6] ;#var - lappend varspecs_trimmed $v_key - } - } - } - } - } - lappend var_names $vname - } - - set result [list var_names $var_names var_class $var_class varspecs_trimmed $varspecs_trimmed] - - proc $cmdname {} [list return $result] - debug.punk.pipe.compile {proc $cmdname} - return $result - } @@ -3269,41 +3306,41 @@ namespace eval punk { #TODO - implement cross-binding (as opposed to overwrite/reassignment) when a var appears multiple times in a pattern/multivar #e.g x@0,x@1 will only match if value at positions 0 & 1 is the same (a form of auto-pinning?) #e.g x,x@0 will only match a single element list - #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) + #todo blocking or - p1|p2 if p1 matches - return p1 and continue pipeline - immediately return p2 if p1 didn't match. (ie p2 not forwarded in pipeline) # non-blocking or - p1||p2 if p1 matches - return p1 and continue pipeline - else match p2 and continue pipeline proc _multi_bind_result {multivar data args} { #puts stdout "---- _multi_bind_result multivar:'$multivar' data:'$data' options:'$args'" - #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 + #'ismatch' must always be first element of dict - as we dispatch on ismatch 0 or ismatch 1 if {![string length $multivar]} { #treat the absence of a pattern as a match to anything #JMN2 - changed to list based destructuring return [dict create ismatch 1 result $data setvars {} script {}] #return [dict create ismatch 1 result [list $data] setvars {} script {}] } - set returndict [dict create ismatch 0 result "" setvars {}] - set script "" + set returndict [dict create ismatch 0 result "" setvars {}] + set script "" - set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] - set opts [dict merge $defaults $args] - set unset [dict get $opts -unset] - set lvlup [dict get $opts -levelup] - set get_mismatchinfo [dict get $opts -mismatchinfo] + set defaults [list -unset 0 -levelup 2 -mismatchinfo 1 -script 0] + set opts [dict merge $defaults $args] + set unset [dict get $opts -unset] + set lvlup [dict get $opts -levelup] + set get_mismatchinfo [dict get $opts -mismatchinfo] #first classify into var_returntype of either "pipeline" or "segment" #segment returntype is indicated by leading % - set varinfo [_var_classify $multivar] - set var_names [dict get $varinfo var_names] - set var_class [dict get $varinfo var_class] - set varspecs_trimmed [dict get $varinfo varspecs_trimmed] + set varinfo [punk::pipe::lib::_var_classify $multivar] + set var_names [dict get $varinfo var_names] + set var_class [dict get $varinfo var_class] + set varspecs_trimmed [dict get $varinfo varspecs_trimmed] set var_actions [list] set expected_values [list] #e.g {a = abc} {b set ""} foreach classinfo $var_class vname $var_names { - lassign [lindex $classinfo 0] v + lassign [lindex $classinfo 0] v lappend var_actions [list $v "" ""] ;#varactions keeps original lhs - not trimmed version lappend expected_values [list var $vname spec $v info - lhs - rhs -] ;#code looks for 'info -' to see if changed from default } @@ -3314,7 +3351,7 @@ namespace eval punk { #puts stdout "\n var_class: $var_class\n" # e.g {{x {}} 0} {{y @0} 0} {{'ok @0} 1} {{^v @@key} 2} - + #set varspecs_trimmed [lmap varinfo $var_class {expr {([lindex $varinfo 1] > 0) ? [list [string range [lindex $varinfo 0 0] 1 end] [lindex $varinfo 0 1]] : [lindex $varinfo 0]}}] #puts stdout "\n varspecs_trimmed: $varspecs_trimmed\n" @@ -3329,18 +3366,18 @@ namespace eval punk { #member lists of returndict which will be appended to in the initial value-retrieving loop set returndict_setvars [dict get $returndict setvars] - + set assigned_values [list] #varname action value - where value is value to be set if action is set - #actions: + #actions: # "" unconfigured - assert none remain unconfigured at end # noop no-change # matchvar-set name is a var to be matched # matchatom-set names is an atom to be matched # matchglob-set - # set + # set # question mark versions are temporary - awaiting a check of action vs var_class # e.g ?set may be changed to matchvar or matchatom or set @@ -3355,7 +3392,7 @@ namespace eval punk { # ^var means a pinned variable - compare value of $var to rhs - don't assign # # In this loop we don't set variables - but assign an action entry in var_actions - all with leading question mark. - # as well as adding the data values to the var_actions list + # as well as adding the data values to the var_actions list # # TODO! we may (commonly) encounter same vkey in the pattern - no need to reparse and re-fetch from data! set vkeys_seen [list] @@ -3396,8 +3433,8 @@ namespace eval punk { dict set returndict setvars $returndict_setvars #assigned_values is the ordered list of source elements in the data (rhs) as extracted by each position-spec - #For booleans the final val may later be normalised to 0 or 1 - + #For booleans the final val may later be normalised to 0 or 1 + #assertion all var_actions were set with leading question mark #perform assignments only if matched ok @@ -3424,7 +3461,7 @@ namespace eval punk { debug.punk.pipe.var {vars: [lsearch -all -inline -index 1 $var_class 6]} 5 debug.punk.pipe.var {globs: [lsearch -all -inline -index 1 $var_class 7]} 5 } - + set match_state [lrepeat [llength $var_names] ?] unset -nocomplain v unset -nocomplain nm @@ -3445,7 +3482,7 @@ namespace eval punk { set class_key [lindex $var_class $i 1] - lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan + lassign {0 0 0 0 0 0 0 0 0 0} isatom ispin isbool isint isdouble isvar isglob isnumeric isgreaterthan islessthan foreach ck $class_key { switch -- $ck { 1 {set isatom 1} @@ -3473,7 +3510,7 @@ namespace eval punk { ##marking numbers with pin ^ has undetermined meaning. Perhaps force expr matching only? #set isgreaterthan [expr {9 in $class_key}] #set islessthan [expr {10 in $class_key}] - + if {$isatom} { @@ -3502,7 +3539,7 @@ namespace eval punk { # - setting expected_values when match_state is set to 0 is ok except for performance - #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or + #todo - pinned booleans? we would need to disambiguate from a direct value match.. ie double tag as something like: ^&var or #ispin may reclassify as isint,isdouble based on contained value (as they don't have their own classifier char and are unambiguous and require special handling) if {$ispin} { #puts stdout "==>ispin $lhsspec" @@ -3512,7 +3549,7 @@ namespace eval punk { upvar $lvlup $varname the_var #if {![catch {uplevel $lvlup [list ::set $varname]} existingval]} {} if {![catch {set the_var} existingval]} { - + if {$isbool} { #isbool due to 2nd classifier i.e ^& lset expected_values $i [list var $varname spec $lhsspec info test-lhs-bool lhs $existingval rhs $val] @@ -3522,7 +3559,7 @@ namespace eval punk { #isglob due to 2nd classifier ^* lset expected_values $i [list var $varname spec $lhsspec info test-lhs-glob lhs $existingval rhs $val] } elseif {$isnumeric} { - #flagged as numeric by user using ^# classifiers + #flagged as numeric by user using ^# classifiers set testexistingval [join [scan $existingval %lld%s] ""] ;# handles octals (leading zeros), ok for use with bignums, internal decimal points and sci notation (but not leading .) if {[string is integer -strict $testexistingval]} { set isint 1 @@ -3533,10 +3570,10 @@ namespace eval punk { set isdouble 1 #doubles comparisons use float_almost_equal - so lhs can differ from rhs - for pins we always want to return the normalised lhs ie exactly what is in the var lset assigned_values $i $existingval - + lset expected_values $i [list var $varname spec $lhsspec info test-lhs-double lhs $existingval rhs $val] } else { - #user's variable doesn't seem to have a numeric value + #user's variable doesn't seem to have a numeric value lset match_state $i 0 lset expected_values $i [list var $varname spec $lhsspec info mismatch-lhs-not-numeric lhs $existingval rhs $val] break @@ -3561,7 +3598,7 @@ namespace eval punk { lset expected_values $i [list var $varname spec $lhsspec info failread-$varname lhs ? rhs $val] break } - } + } } @@ -3583,7 +3620,7 @@ namespace eval punk { if {[string index $lhs 0] eq "."} { set testlhs $lhs } else { - set testlhs [join [scan $lhs %lld%s] ""] + set testlhs [join [scan $lhs %lld%s] ""] } if {[string index $val 0] eq "."} { set testval $val @@ -3648,10 +3685,10 @@ namespace eval punk { } } elseif {[string is digit -strict [string trim $val -]] } { #probably a wideint or bignum with no decimal point - #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . + #It seems odd that bignums which just look like large integers should ever compare equal if you do a +1 to one side . #if we use float_almost_equal they may compare equal. on the other hand expr also does apparently inconsistent thins with comparing integer-like bignums vs similar sized nums with .x at the end. - #2 values further apart can compare equal while int-like ones closer together can compare different. - #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. + #2 values further apart can compare equal while int-like ones closer together can compare different. + #The rule seems to be for bignums that if it *looks* like a whole int the comparison is exact - but otherwise the float behaviours kick in. #This is basically what we're doing here but with an arguably better (for some purposes!) float comparison. #string comparison can presumably always be used as an alternative. # @@ -3682,7 +3719,7 @@ namespace eval punk { } } } else { - if {[punk::float_almost_equal $testlhs $testval]} { + if {[punk::pipe::float_almost_equal $testlhs $testval]} { lset match_state $i 1 } else { if {$isgreaterthan} { @@ -3709,7 +3746,7 @@ namespace eval punk { } } } else { - #e.g rhs not a number.. + #e.g rhs not a number.. if {$testlhs == $testval} { lset match_state $i 1 } else { @@ -3721,7 +3758,7 @@ namespace eval punk { } elseif {$isdouble} { #dragons (and shimmering) # - # + # if {$ispin} { set existing_expected [lindex $expected_values $i] set lhs [dict get $existing_expected lhs] @@ -3761,7 +3798,7 @@ namespace eval punk { } } else { #float_almost_equal will disagree with expr based on scale.. just enough to allow for example [expr 0.2 + 0.1] to equal 0.3 - whereas expr will declare a mismatch - if {[punk::float_almost_equal $lhs $testval]} { + if {[punk::pipe::float_almost_equal $lhs $testval]} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-float-almost-equal lhs $lhs rhs $val] } else { @@ -3777,7 +3814,7 @@ namespace eval punk { # Also - the setting of the variable x is normalized to 1 or 0 only. (true & false would perhaps be nicer - but 1 & 0 are theoretically more efficient for later comparisons as they can have a pure int rep?.) # todo - consider the booleanString rep. Can/should we return true & false instead and maintain efficiency w.r.t shimmering? # - #punk::boolean_equal $a $b + #punk::pipe::boolean_equal $a $b set extra_match_info "" ;# possible crossbind indication set is_literal_boolean 0 if {$ispin} { @@ -3789,7 +3826,7 @@ namespace eval punk { set lhs [string range $lhsspec 1 end] ;# - strip off & classifier prefix if {![string length $lhs]} { - #empty varname - ok + #empty varname - ok if {[string is boolean -strict $val] || [string is double -strict $val]} { lset match_state $i 1 lset var_actions $i 1 "return-normalised-value" @@ -3813,7 +3850,7 @@ namespace eval punk { set tclvar $lhs if {[string is double $tclvar]} { error "pipesyntax invalid variable name '$tclvar' for boolean in pattern. (subset of legal tcl vars allowed in pattern context)" "_multi_bind_result $multivar $data $args" [list pipesyntax patternvariable invalid_boolean $tclvar] - #proc _multi_bind_result {multivar data args} + #proc _multi_bind_result {multivar data args} } #treat as variable - need to check cross-binding within this pattern group set first_bound [lsearch -index 0 $var_actions $lhsspec] @@ -3846,7 +3883,7 @@ namespace eval punk { #may have already matched above..(for variable) if {[lindex $match_state $i] != 1} { - if {![catch {punk::boolean_almost_equal $lhs $val} ismatch]} { + if {![catch {punk::pipe::boolean_almost_equal $lhs $val} ismatch]} { if {$ismatch} { lset match_state $i 1 lset expected_values $i [list var $varname spec $lhsspec info match-boolean-almost-equal$extra_match_info lhs $lhs rhs $val] @@ -3880,11 +3917,11 @@ namespace eval punk { } } elseif {$ispin} { - #handled above.. leave case in place so we don't run else for pins + #handled above.. leave case in place so we don't run else for pins } else { #puts stdout "==> $lhsspec" - #NOTE - pinned var of same name is independent! + #NOTE - pinned var of same name is independent! #ie ^x shouldn't look at earlier x bindings in same pattern #unpinned non-atoms #cross-binding. Within this multivar pattern group only (use pin ^ for binding to result from a previous pattern) @@ -3904,7 +3941,7 @@ namespace eval punk { } default { set first_bound [lsearch -index 0 $var_actions $varname] - #assertion first_bound >=0, we will always find something - usually self + #assertion first_bound >=0, we will always find something - usually self if {$first_bound == $i} { lset match_state $i 1 lset var_actions $i 1 [string range $act 1 end] ;# ?set -> set @@ -3964,7 +4001,7 @@ namespace eval punk { if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { #isvar if {[lindex $var_actions $i 1] eq "set"} { - upvar $lvlup $varname the_var + upvar $lvlup $varname the_var set the_var [lindex $var_actions $i 2] } } @@ -3976,7 +4013,7 @@ namespace eval punk { # if {([lindex $var_class $i 1] == 6 || [lindex $var_class $i] == 3 ) && ([string length [set varname [lindex $var_names $i]]])} { # #isvar # lassign $va lhsspec act val - # upvar $lvlup $varname the_var + # upvar $lvlup $varname the_var # if {$act eq "set"} { # set the_var $val # } @@ -3990,7 +4027,8 @@ namespace eval punk { #todo - some way to restrict mismatch info to simple "mismatch" and avoid overhead of verbose message #e.g for within pipeswitch block where mismatches are expected and the reasons are less important than moving on quickly set vidx 0 - set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + #set mismatches [lmap m $match_state v $var_names {expr {$m == 0} ? {[list mismatch $v]} : {[list match $v]}}] + set mismatches [lmap m $match_state v $var_names {expr {$m == 0 ? [list mismatch $v] : [list match $v]}}] set var_display_names [list] foreach v $var_names { if {$v eq ""} { @@ -3999,7 +4037,9 @@ namespace eval punk { lappend var_display_names $v } } - set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + #REVIEW 2025 + #set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0} ? {$v} : {[expr {$m eq "?"} ? {"?[string repeat { } [expr [string length $v] -1]]"} : {[string repeat " " [string length $v]]} ]}}] + set mismatches_display [lmap m $match_state v $var_display_names {expr {$m == 0 ? $v : [expr {$m eq "?" ? "?[string repeat { } [expr {[string length $v] -1}]]" : [string repeat " " [string length $v]] }]}}] set msg "\n" append msg "Unmatched\n" append msg "Cannot match right hand side to pattern $multivar\n" @@ -4015,12 +4055,12 @@ namespace eval punk { #6 - var #7 - glob (no classifier and contains * or ?) foreach mismatchinfo $mismatches { - lassign $mismatchinfo status varname + lassign $mismatchinfo status varname if {$status eq "mismatch"} { # varname can be empty string set varclass [lindex $var_class $i 1] set val [lindex $var_actions $i 2] - set e [dict get [lindex $expected_values $i] lhs] + set e [dict get [lindex $expected_values $i] lhs] set type "" if {2 in $varclass} { append type "pinned " @@ -4098,7 +4138,7 @@ namespace eval punk { return [dict get $d result] } } - # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch + # initially promising - but the approach runs into impossible disambiguation of mismatch as data vs an actual mismatch proc _handle_bind_result_experimental1 {d} { #set match_caller [info level 2] #debug.punk.pipe {_handle_bind_result match_caller: $match_caller} 9 @@ -4122,55 +4162,43 @@ namespace eval punk { upvar $pipevarname the_pipe set the_pipe $args } - + #pipealias should capture the namespace context of the pipeline so that commands are resolved in the namespace in which the pipealias is created proc pipealias {targetcmd args} { set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] - tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] + tailcall interp alias {} $targetcmd {} apply [list args [append cmdcopy " {*}\$args"] $nscaller] } proc pipealias_extract {targetcmd} { set applybody [lindex [interp alias "" $targetcmd] 1 1] #strip off trailing " {*}$args" - return [lrange [string range $applybody 0 end-9] 0 end] + return [lrange [string range $applybody 0 end-9] 0 end] } #although the pipealias2 'concat' alias is cleaner in that the original pipeline can be extracted using list commands - it runs much slower proc pipealias2 {targetcmd args} { - set cmdcopy [punk::objclone $args] + set cmdcopy [punk::objclone $args] set nscaller [uplevel 1 [list namespace current]] tailcall interp alias {} $targetcmd {} apply [list args [concat "\[concat" [list $cmdcopy] "\$args]"] $nscaller] } - #map rhs to names suitable to use in pipemcd proc name (whitespace mapping) - # (for .= and = pipecmds) - proc pipecmd_namemapping {rhs} { - #used to build a command from a pattern which could contain :: - we need to map it to keep it a single command in the desired namespace. - #glob chars will prevent us using simple test {[info commands $cmd] eq ""} to test existence - #we could use the test {$cmd in [info commands]} - but mapping away globchars is more robust, allowing the simpler test - set rhs [string trim $rhs];#ignore all leading & trailing whitespace - set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token - set rhs [tcl::string::map {: ? * } $rhs] - #review - we don't expect other command-incompatible chars such as colon? - return $rhs - } #same as used in unknown func for initial launch - #variable re_assign {^([^\r\n=\{]*)=(.*)} - #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} variable re_assign {^([^ \t\r\n=\{]*)=(.*)} variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #match_assign is tailcalled from unknown - uplevel 1 gets to caller level proc match_assign {scopepattern equalsrhs args} { - #review - :: is legal in atoms! + #review - :: is legal in atoms! if {[string match "*::*" $scopepattern]} { error "match_assign scopepattern '$scopepattern' contains namespace separator '::' - invalid." } #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args" set fulltail $args set cmdns ::punk::pipecmds - set namemapping [pipecmd_namemapping $equalsrhs] + set namemapping [punk::pipe::lib::pipecmd_namemapping $equalsrhs] - #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW + #we deliberately don't call pipecmd_namemapping on the scopepattern even though it may contain globs. REVIEW #(we need for example x*= to be available as is via namespace path mechanism (from punk::pipecmds namespace)) set pipecmd ${cmdns}::$scopepattern=$namemapping @@ -4189,10 +4217,10 @@ namespace eval punk { #NOTE: #we need to ensure for case: - #= x=y + #= x=y #that the second arg is treated as a raw value - never a pipeline command - #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = + #equalsrhs is set if there is a segment-insertion-pattern *directly* after the = #debug.punk.pipe {match_assign '$multivar' '$equalsrhs' '$fulltail'} 4 #can match pattern on lhs with a value where pattern is a minilang that can refer to atoms (simple non-whitespace strings), numbers, or varnames (possibly pinned) as well as a trailing spec for position within the data. @@ -4202,7 +4230,7 @@ namespace eval punk { # in our script's handling of args: #avoid use of regexp match on each element - or we will unnecessarily force string reps on lists - #same with lsearch with a string pattern - + #same with lsearch with a string pattern - #wouldn't matter for small lists - but we need to be able to handle large ones efficiently without unneccessary string reps set script [string map [list $scopepattern $equalsrhs] { #script built by punk::match_assign @@ -4210,7 +4238,7 @@ namespace eval punk { #scan for existence of any pipe operator (|*> or <*|) only - we don't need position #all pipe operators must be a single element #we don't first check llength args == 1 because for example: - # x= <| + # x= <| # x= |> #both leave x empty. To assign a pipelike value to x we would have to do: x= <| |> (equiv: set x |>) foreach a $args { @@ -4239,14 +4267,14 @@ namespace eval punk { # as we aren't in a pipleine - there is no data to insert - we proably still need to run _split_equalsrhs to verify the syntax. # review - consider way to turn it off as optimisation for non-pipelined assignment - but generally standard Tcl set could be used for that purpose. # We are probably only here if testing in the repl - in which case the error messages are important. - set var_index_position_list [_split_equalsrhs $equalsrhs] + set var_index_position_list [punk::pipe::lib::_split_equalsrhs $equalsrhs] #we may have an insertion-spec that inserts a literal atom e.g to wrap in "ok" # x='ok'>0/0 data # => {ok data} - # we won't examine for vars as there is no pipeline - ignore + # we won't examine for vars as there is no pipeline - ignore # also ignore trailing * (indicator for variable data to be expanded or not - ie {*}) # we will differentiate between / and @ in the same way that general pattern matching works. - # /x will simply call linsert without reference to length of list + # /x will simply call linsert without reference to length of list # @x will check for out of bounds # # !TODO - sort by position lowest to highest? or just require user to order the pattern correctly? @@ -4259,7 +4287,7 @@ namespace eval punk { #Here, we are not assigning to v1 - but matching the index spec /0 with the data from v1 #ie Y is inserted at position 0 to get A Y #(Note the difference from lhs) - #on lhs v1/1= {X Y} + #on lhs v1/1= {X Y} #would pattern match against the *data* A B and set v1 to B #in this point of an assign (= as opposed to .=) IF we have already determined there is no trailing pipeline @@ -4268,10 +4296,10 @@ namespace eval punk { #eg out= list a $callervar c #or alternatively use .= instead # - #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments + #HOWEVER - we need to build/compile a script that could then have further pipeline elements supplied as arguments #At the moment - this is handled in the script above by diverting to punk::pipeline to handle #The only vars/data we can possibly have to insert, come from the ] }] - set needs_insertion 0 + set needs_insertion 0 } if {$needs_insertion} { set script2 [punk::list_insertion_script $positionspec segmenttail ] set script2 [string map [list "\$insertion_data" ] $script2] append script $script2 - } + } + - } } - if {![string length $scopepattern]} { + if {![string length $scopepattern]} { append script { return $segmenttail } } else { append script [string map [list $scopepattern] { #we still need to bind whether list is empty or not to allow any patternmatch to succeed/fail - set d [punk::_multi_bind_result {} $segmenttail] + set d [punk::_multi_bind_result {} $segmenttail] #return [punk::_handle_bind_result $d] - #maintenance: inlined + #maintenance: inlined if {![dict exists $d result]} { #uplevel 1 [list error [dict get $d mismatch]] #error [dict get $d mismatch] @@ -4356,7 +4384,7 @@ namespace eval punk { tailcall $pipecmd {*}$args } - #return a script for inserting data into listvar + #return a script for inserting data into listvar #review - needs updating for list-return semantics of patterns? proc list_insertion_script {keyspec listvar {data }} { set positionspec [string trimright $keyspec "*"] @@ -4384,15 +4412,15 @@ namespace eval punk { } elseif {$isint || [regexp {^(end|end[-+]{1,2}[0-9]+)$} $index]} { if {$ptype eq "@"} { #compare position to *possibly updated* list - note use of $index > $datalen rather than $index+1 > $datalen - (we allow 'insertion' at end of list by numeric index) - if {$isint} { + if {$isint} { append script [string map [list $listvar $index] { if {( > [llength $])} { - #not a pipesyntax error + #not a pipesyntax error error "pipedata insertionpattern index out of bounds. index: vs len: [llength $] use /x instead of @x to avoid check (list_insertion_script)" "list_insertion_script $keyspec" [list pipedata insertionpattern index_out_f_bounds] } }] } - #todo check end-x bounds? + #todo check end-x bounds? } if {$isint} { append script [string map [list $listvar $index $exp $data] { @@ -4455,98 +4483,20 @@ namespace eval punk { }] } - + } else { error "pipesyntax error in segment - positionspec:'$keyspec' unable to interpret position spec (list_insertion_script)4" "list_insertion_script $keyspec" [list pipesyntax insertionpattern_invalid] - } + } return $script } - #todo - consider whether we can use < for insertion/iteration combinations - # =a<,b< iterate once through - # =a><,b>< cartesian product - # =a<>,b<> ??? zip ? - # - # ie = {a b c} |> .=< inspect - # would call inspect 3 times, once for each argument - # .= list {a b c} {x y z} |a/0,b/1> .=a><,b>< list - # would produce list of cartesian pairs? - # - proc _split_equalsrhs {insertionpattern} { - #map the insertionpattern so we can use faster globless info command search - set name_mapped [pipecmd_namemapping $insertionpattern] - set cmdname ::punk::pipecmds::split_rhs::_$name_mapped - if {[info commands $cmdname] ne ""} { - return [$cmdname] - } - - set lst_var_indexposition [punk::_split_patterns_memoized $insertionpattern] - set i 0 - set return_triples [list] - foreach v_pos $lst_var_indexposition { - lassign $v_pos v index_and_position - #e.g varname@@data/ok>0 varname/1/0>end - #ensure only one ">" is detected - if {![string length $index_and_position]} { - set indexspec "" - set positionspec "" - } else { - set chars [split $index_and_position ""] - set posns [lsearch -all $chars ">"] - if {[llength $posns] > 1} { - error "pipesyntax error in segment insertion pattern '$insertionpattern' -v '$v' multiple '>' characters. Pattern not understood." "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - if {![llength $posns]} { - set indexspec $index_and_position - set positionspec "" - } else { - set splitposn [lindex $posns 0] - set indexspec [string range $index_and_position 0 $splitposn-1] - set positionspec [string range $index_and_position $splitposn+1 end] - } - } - - #review - - if {($positionspec in [list "*" "/*" "@*" "/" "@"]) || ($v eq "*" && $positionspec eq "")} { - set star "" - if {$v eq "*"} { - set v "" - set star "*" - } - if {[string index $positionspec end] eq "*"} { - set star "*" - } - #it is always possible to insert at end of list regardless of current length - so /end* and @end* are equivalent - #as are /end and @end - #lset lst_var_indexposition $i [list $v "/end$star"] - set triple [list $v $indexspec "/end$star"] - } else { - if {$positionspec eq ""} { - #e.g just =varname - #lset lst_var_indexposition $i [list $v "/end"] - set triple [list $v $indexspec "/end"] - #error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' missing position spec e.g /0" - } else { - if {[string index $indexspec 0] ni [list "" "/" "@"]} { - error "pipesyntax error in segment insertionpattern '$insertionpattern' - v '$v' bad index spec '$indexspec'" "_split_equalsrhs $insertionpattern" [list pipesyntax insertionpattern_invalid] - } - set triple [list $v $indexspec $positionspec] - } - } - lappend return_triples $triple - incr i - } - proc $cmdname {} [list return $return_triples] - return $return_triples - } - proc _is_math_func_prefix {e1} { #also catch starting brackets.. e.g "(min(4,$x) " if {[regexp {^[^[:alnum:]]*([[:alnum:]]*).*} $e1 _ word]} { - #possible math func + #possible math func if {$word in [info functions]} { return true } @@ -4583,8 +4533,8 @@ namespace eval punk { #puts "PERCENTS : $percents" set sequences [list] set in_sequence 0 - set start -1 - set end -1 + set start -1 + set end -1 set i 0 #todo - some more functional way of zipping/comparing these lists? set s_length 0 ;#sequence length including % symbols - minimum for tag therefore 2 @@ -4601,7 +4551,7 @@ namespace eval punk { } else { if {$n ^ $p} { incr s_length - incr end + incr end } else { if {$n & $p} { if {$s_length == 1} { @@ -4612,7 +4562,7 @@ namespace eval punk { set start $i set end $i } else { - incr end + incr end lappend sequences [list $start $end] set in_sequence 0 set s_length 0 @@ -4649,81 +4599,11 @@ namespace eval punk { return $output } - # - # - # relatively slow on even small sized scripts - proc arg_is_script_shaped2 {arg} { - set re {^(\s|;|\n)$} - set chars [split $arg ""] - if {[lsearch -regex $chars $re] >=0} { - return 1 - } else { - return 0 - } - } - - #exclude quoted whitespace - proc arg_is_script_shaped {arg} { - if {[tcl::string::first \n $arg] >= 0} { - return 1 - } elseif {[tcl::string::first ";" $arg] >= 0} { - return 1 - } elseif {[tcl::string::first " " $arg] >= 0 || [tcl::string::first \t $arg] >= 0} { - lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found - return [expr {$part2 ne ""}] - } else { - return 0 - } - } - proc _rhs_tail_split {fullrhs} { - set inq 0; set indq 0 - set equalsrhs "" - set i 0 - foreach ch [split $fullrhs ""] { - if {$inq} { - append equalsrhs $ch - if {$ch eq {'}} { - set inq 0 - } - } elseif {$indq} { - append equalsrhs $ch - if {$ch eq {"}} { - set indq 0 - } - } else { - switch -- $ch { - {'} { - set inq 1 - } - {"} { - set indq 1 - } - " " { - #whitespace outside of quoting - break - } - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - a - b - c - d - e - f - g - h - i - j - k - l - m - n - o - p - q - r - s - t - u - v - w - x - y - z {} - default { - #\t not a literal for switch jumpTable bytecompile - review - can we do it without for example string mapping to (and without a literal binary tab in source file)? - #we can't (reliably?) put \t as one of our switch keys - # - if {$ch eq "\t"} { - break - } - } - } - append equalsrhs $ch - } - incr i - } - set tail [tcl::string::range $fullrhs $i end] - return [list $equalsrhs $tail] - } # -- #consider possible tilde templating version ~= vs .= - #support ~ and ~* placeholders only. - #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* + #support ~ and ~* placeholders only. + #e.g x~= list aa b c |> lmap v ~ {string length $v} |> tcl::mathfunc::max ~* #The ~ being mapped to $data in the pipeline. #This is more readable and simpler for beginners - although it doesn't handle more advanced insertion requirements. #possibility to mix as we can already with .= and = @@ -4739,12 +4619,14 @@ namespace eval punk { #--------------------------------------------------------------------- # test if we have an initial x.=y.= or x.= y.= - + #nextail is tail for possible recursion based on first argument in the segment - set nexttail [lassign $fulltail next1] ;#tail head + #set nexttail [lassign $fulltail next1] ;#tail head + set next1 [lindex $args 0] switch -- $next1 { pipematch { + set nexttail [lrange $args 1 end] set results [uplevel 1 [list pipematch {*}$nexttail]] debug.punk.pipe {>>> pipematch results: $results} 1 @@ -4773,9 +4655,9 @@ namespace eval punk { #The second element is always treated as a raw value - not a pipeline instruction. #whereas... for execution: #.= x=y the second element is a pipeline-significant symbol based on the '=' even if it was passed in as an argument. - #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - + #Usually an execution segment (.= cmd etc..) will have args inserted at the tail anyway - #- but if the pipeline is designed to put an argument in the zero position - then presumably it is intended as a pipeline-significant element anyway - #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines + #This gives a *slight* incompatibility with external commands containing '=' - in that they may not work properly in pipelines # if {$segment_op ne "="} { #handle for example: @@ -4784,7 +4666,8 @@ namespace eval punk { #var1 will contain ETC (from entire pipeline), var2 will contain etc (from associated segment) # - if {([set nexteposn [string first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { + if {([set nexteposn [string last = $next1]] >= 0) && (![punk::pipe::lib::arg_is_script_shaped $next1]) } { + set nexttail [lrange $args 1 end] #*SUB* pipeline recursion. #puts "======> recurse based on next1:$next1 " if {[string index $next1 $nexteposn-1] eq {.}} { @@ -4794,7 +4677,7 @@ namespace eval punk { #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5 #debug.punk.pipe {>>> results: $results} 1 return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]] - } + } #puts "======> recurse assign based on next1:$next1 " #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #} @@ -4819,17 +4702,17 @@ namespace eval punk { set more_pipe_segments 1 ;#first loop #this contains the main %data% and %datalist% values going forward in the pipeline - #as well as any extra pipeline vars defined in each |> + #as well as any extra pipeline vars defined in each |> #It also contains any 'args' with names supplied in <| set dict_tagval [dict create] ;#cumulative %x% tag dict which operates on the whole length of the pipeline #determine if there are input args at the end of the pipeline indicated by reverse <| symbol possibly with argspecs e.g transform x y z =0} { - set tailremaining [lrange $fulltail 0 $firstargpipe_posn-1] - set argslist [lrange $fulltail $firstargpipe_posn+1 end] ;#Note that this could be a whole other pipeline with |> and/or <| elements. - set argpipe [lindex $fulltail $firstargpipe_posn] - set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " and/or <| elements. + set argpipe [lindex $fulltail $firstargpipe_posn] + set argpipespec [string range $argpipe 1 end-1] ;# strip off < & | from " 0}] #if {$segment_has_insertions} { # puts stdout ">>> $segment_members insertion_patterns $insertion_patterns" @@ -4994,7 +4877,7 @@ namespace eval punk { foreach {vname val} $pipedvars { #add additionally specified vars and allow overriding of %args% and %data% by not setting them here if {$vname eq "data"} { - #already potentially overridden + #already potentially overridden continue } dict set dict_tagval $vname $val @@ -5010,7 +4893,7 @@ namespace eval punk { #add previous_result as data in end position by default, only if *no* insertions specified (data is just list-wrapped previous_result) #set segment_members_filled [concat $segment_members [dict get $dict_tagval %data%]] ;# data flows through by default as single element - not args - because some strings are not valid lists #insertion-specs with a trailing * can be used to insert data in args format - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { lappend segment_members_filled [dict get $dict_tagval data] } @@ -5020,7 +4903,7 @@ namespace eval punk { set segment_members_filled [list] set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign - set rhsmapped [pipecmd_namemapping $rhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $rhs] set cmdname "::punk::pipecmds::insertion::_$rhsmapped" #glob chars have been mapped - so we can test by comparing info commands result to empty string if {[info commands $cmdname] eq ""} { @@ -5057,13 +4940,14 @@ namespace eval punk { } if {[dict exists $dict_tagval $v]} { set insertion_data [dict get $dict_tagval $v] - #todo - use destructure_func + #todo - use destructure_func set d [punk::_multi_bind_result $indexspec $insertion_data] set insertion_data [punk::_handle_bind_result $d] } else { #review - skip error if varname is 'data' ? #e.g we shouldn't really fail for: #.=>* list a b c <| + #??? Technically #we need to be careful not to insert empty-list as an argument by default error "pipevariable - varname $v not present in pipeline context. pipecontext_vars: [dict keys $dict_tagval] (2)" " pipecontext_vars: [dict keys $dict_tagval]" [list pipevariable variable_not_in_pipeline_scope] } @@ -5098,9 +4982,9 @@ namespace eval punk { #set segment_members_filled $segmenttail #note - length of segment_members_filled may now differ from length of original segment_members! (if do_expand i.e trailing * in any insertion_patterns) - + } - set rhs [string map $dict_tagval $rhs] ;#obsolete? + set rhs [string map $dict_tagval $rhs] ;#obsolete? debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 @@ -5109,8 +4993,8 @@ namespace eval punk { #we require re_dot_assign before re_assign (or fix regexes so order doesn't matter!) if {(!$segment_first_is_script ) && $segment_op eq ".="} { - #no scriptiness detected - + #no scriptiness detected + #debug.punk.pipe.rep {[a yellow bold][rep_listname segment_members_filled][a]} 4 set cmdlist_result [uplevel 1 $segment_members_filled] @@ -5119,25 +5003,25 @@ namespace eval punk { #set d [_multi_bind_result $returnvarspec [punk::K $cmdlist_result [unset cmdlist_result]]] set d [_multi_bind_result $returnvarspec [lindex [list $cmdlist_result [unset cmdlist_result ]] 0]] - + set segment_result [_handle_bind_result $d] #puts stderr ">>forward_result: $forward_result segment_result $segment_result" } elseif {$segment_op eq "="} { - #slightly different semantics for assigment! - #We index into the DATA - not the position within the segment! + #slightly different semantics for assigment! + #We index into the DATA - not the position within the segment! #(an = segment must take a single argument, as opposed to a .= segment) #(This was a deliberate design choice for consistency with set, and to reduce errors.) #(we could have allowed multiple args to = e.g to form a list, but it was tried, and the edge-cases were unintuitive and prone to user error) #(The choice to restrict to single argument, but allow insertion and appending via insertion-specs is more explicit and reliable even though the insertion-specs operate differently to those of .=) # - #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data - #v= {a b c} |> = + #we have to ensure that for an empty segment - we don't append to the empty list, thus listifying the data + #v= {a b c} |> = # must return: {a b c} not a b c # if {!$segment_has_insertions} { - set segment_members_filled $segment_members + set segment_members_filled $segment_members if {[dict exists $dict_tagval data]} { if {![llength $segment_members_filled]} { set segment_members_filled [dict get $dict_tagval data] @@ -5168,7 +5052,7 @@ namespace eval punk { lappend segmentargnames $k lappend segmentargvals $val } - + set argsdatalist $prevr ;#default is raw result as a list. May be overridden by an argspec within |> e.g |args@@key> or stripped if not a tcl list #puts "------> rep prevr argsdatalist: [rep $argsdatalist]" set add_argsdata 0 @@ -5255,7 +5139,7 @@ namespace eval punk { #It makes more sense and is ultimately more useful (and more easy to reason about) for the result of each assignment to be related only to the pre-pipe section #It may however make a good debug point #puts stderr "segment $i segment_result:$segment_result" - + debug.punk.pipe.rep {[rep_listname segment_result]} 3 @@ -5265,17 +5149,17 @@ namespace eval punk { #examine tailremaining. # either x x x |?> y y y ... # or just y y y - #we want the x side for next loop - + #we want the x side for next loop + #set up the conditions for the next loop - #|> x=y args + #|> x=y args # inpipespec - contents of previous piper |xxx> # outpipespec - empty or content of subsequent piper |xxx> - # previous_result + # previous_result # assignment (x=y) - set pipespec($j,in) $pipespec($i,out) + set pipespec($j,in) $pipespec($i,out) set outpipespec "" set tailmap "" set next_pipe_posn -1 @@ -5295,7 +5179,7 @@ namespace eval punk { if {[llength $tailremaining] || $next_pipe_posn >= 0} { if {$next_pipe_posn >=0} { - set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for + set next_all_members [lrange $tailremaining 0 $next_pipe_posn-1] ;#exclude only piper |xxx> for set tailremaining [lrange $tailremaining $next_pipe_posn+1 end] } else { @@ -5311,7 +5195,7 @@ namespace eval punk { set rhs "" set segment_first_is_script 0 if {[llength $next_all_members]} { - if {[arg_is_script_shaped [lindex $next_all_members 0]]} { + if {[punk::pipe::lib::arg_is_script_shaped [lindex $next_all_members 0]]} { set segment_first_word [lindex $next_all_members 0] set segment_first_is_script 1 set segment_op "" @@ -5322,7 +5206,7 @@ namespace eval punk { if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op ".=" set segment_first_word [lindex $next_all_members 1] - set script_like_first_word [arg_is_script_shaped $segment_first_word] + set script_like_first_word [punk::pipe::lib::arg_is_script_shaped $segment_first_word] if {$script_like_first_word} { set segment_first_is_script 1 ;#relative to segment_members which no longer includes the .= } @@ -5330,7 +5214,7 @@ namespace eval punk { } elseif {[regexp {^([^ \t\r\n=]*)=(.*)} $possible_assignment _ returnvarspec rhs]} { set segment_op "=" #never scripts - #must be at most a single element after the = ! + #must be at most a single element after the = ! if {[llength $next_all_members] > 2} { #raise this as pipesyntax as opposed to pipedata? error "pipesyntax - at most one element can follow = (got [lrange $next_all_members 1 end])" "pipeline $segment_op $returnvarspec $rhs [lrange $next_all_members 1 end]" [list pipesyntax too_many_elements] @@ -5341,7 +5225,7 @@ namespace eval punk { } else { set segment_is_list 1 ;#only used for segment_op = } - + set segment_members $segment_first_word } else { #no assignment operator and not script shaped @@ -5357,7 +5241,7 @@ namespace eval punk { } else { #?? two pipes in a row ? - debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 + debug.punk.pipe {[a+ yellow bold]WARNING: no segment members found[a]} 0 set segment_members return set segment_first_word return } @@ -5369,7 +5253,7 @@ namespace eval punk { } else { debug.punk.pipe {[a+ cyan bold]End of pipe segments ($i)[a]} 4 #output pipe spec at tail of pipeline - + set pipedvars [dict create] if {[string length $pipespec($i,out)]} { set d [apply {{mv res} { @@ -5382,7 +5266,7 @@ namespace eval punk { set more_pipe_segments 0 } - #the segment_result is based on the leftmost var on the lhs of the .= + #the segment_result is based on the leftmost var on the lhs of the .= #whereas forward_result is always the entire output of the segment #JMN2 #lappend segment_result_list [join $segment_result] @@ -5414,7 +5298,7 @@ namespace eval punk { } set s $posn } else { - #int + #int if {($start < 0) || ($start > ($datalen -1))} { return 0 } @@ -5430,7 +5314,7 @@ namespace eval punk { } set e $posn } else { - #int + #int if {($end < 0)} { return 0 } @@ -5448,7 +5332,7 @@ namespace eval punk { if {$e < $s} { return 0 } - + return [expr {$e - $s + 1}] } @@ -5601,11 +5485,11 @@ namespace eval punk { #windows experiment todo - use twapi and named pipes - #twapi::namedpipe_server {\\.\pipe\something} + #twapi::namedpipe_server {\\.\pipe\something} #Then override tcl 'exec' and replace all stdout/stderr/stdin with our fake ones #These can be stacked with shellfilter and operate as OS handles - which we can't do with fifo2 etc # - + if {[string first " " $new] > 0} { set c1 $name } else { @@ -5619,8 +5503,8 @@ namespace eval punk { #when using exec with >&@stdout (to ensure process is connected to console) - the output unfortunately doesn't go via the shellfilter stacks #lappend idlist_stderr [shellfilter::stack::add stderr ansiwrap -settings {-colour {red bold}}] #lappend idlist_stdout [shellfilter::stack::add stdout tee_to_var -action float -settings {-varname ::shellrun::runout}] - - if {[dict get $::punk::config::running auto_exec_mechanism] eq "experimental"} { + + if {[punk::config::configure running auto_exec_mechanism] eq "experimental"} { #TODO - something cross-platform that allows us to maintain a separate console(s) with an additional set of IO channels to drive it #not a trivial task @@ -5628,16 +5512,16 @@ namespace eval punk { #VIM for example won't run, and various programs can't detect terminal dimensions etc and/or will default to ansi-free output #ctrl-c propagation also needs to be considered - set teehandle punksh + set teehandle punksh uplevel 1 [list ::catch \ [list ::shellfilter::run [concat [list $new] [lrange $args 1 end]] -teehandle $teehandle -inbuffering line -outbuffering none ] \ ::tcl::UnknownResult ::tcl::UnknownOptions] if {[string trim $::tcl::UnknownResult] ne "exitcode 0"} { - dict set ::tcl::UnknownOptions -code error + dict set ::tcl::UnknownOptions -code error set ::tcl::UnknownResult "Non-zero exit code from command '$args' $::tcl::UnknownResult" } else { - #no point returning "exitcode 0" if that's the only non-error return. + #no point returning "exitcode 0" if that's the only non-error return. #It is misleading. Better to return empty string. set ::tcl::UnknownResult "" } @@ -5647,10 +5531,10 @@ namespace eval punk { set redir ">&@stdout <@stdin" uplevel 1 [list ::catch [concat exec $redir $new [lrange $args 1 end]] ::tcl::UnknownResult ::tcl::UnknownOptions] - #we can't detect stdout/stderr output from the exec - #for now emit an extra \n on stderr + #we can't detect stdout/stderr output from the exec + #for now emit an extra \n on stderr #todo - there is probably no way around this but to somehow exec in the context of a completely separate console - #This is probably a tricky problem - especially to do cross-platform + #This is probably a tricky problem - especially to do cross-platform # # - use [dict get $::tcl::UnknownOptions -code] (0|1) exit if {[dict get $::tcl::UnknownOptions -code] == 0} { @@ -5747,7 +5631,7 @@ namespace eval punk { } } - + } return -code error -errorcode [list TCL LOOKUP COMMAND $name] "invalid command name $name" @@ -5756,11 +5640,12 @@ namespace eval punk { proc know {cond body} { set existing [info body ::unknown] #assuming we can't test on cond being present in existing unknown script - because it may be fairly simple and prone to false positives (?) - ##This means we can't have 2 different conds with same body if we test for body in unknown. + ##This means we can't have 2 different conds with same body if we test for body in unknown. ##if {$body ni $existing} { - package require base64 set scr [base64::encode -maxlen 0 $cond] ;#will only be decoded if the debug is triggered #tcllib has some double-substitution going on.. base64 seems easiest and will not impact the speed of normal execution when debug off. + + #tclint-disable-next-line proc ::unknown {args} [string map [list @c@ $cond @b@ $body @scr@ $scr] { #--------------------------------------- if {![catch {expr {@c@}} res] && $res} { @@ -5779,7 +5664,6 @@ namespace eval punk { } proc decodescript {b64} { if {[ catch { - package require base64 base64::decode $b64 } scr]} { return "" @@ -5817,36 +5701,36 @@ namespace eval punk { if {[info commands ::tsv::set] eq ""} { puts stderr "set_repl_last_unknown - tsv unavailable!" return - } + } tsv::set repl last_unknown {*}$args } # --------------------------- + #---------------- + #for var="val {a b c}" + #proc ::punk::val {{v {}}} {tailcall lindex $v} + #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version + + proc ::punk::val [list [list v [purelist]]] {return $v} + #---------------- proc configure_unknown {} { #----------------------------- #these are critical e.g core behaviour or important for repl displaying output correctly - - #---------------- - #for var="val {a b c}" - #proc ::punk::val {{v {}}} {tailcall lindex $v} - #proc ::punk::val {{v {}}} {return $v} ;#2023 - approx 2x faster than the tailcall lindex version - proc ::punk::val [list [list v [purelist]]] {return $v} - #---------------- + #can't use know - because we don't want to return before original unknown body is called. proc ::unknown {args} [string cat { - package require base64 #set ::punk::last_run_display [list] #set ::repl::last_unknown [lindex $args 0] ;#jn #tsv::set repl last_unknown [lindex $args 0] ;#REVIEW - punk::set_repl_last_unknown [lindex $args 0] + punk::set_repl_last_unknown [lindex $args 0] }][info body ::unknown] #handle process return dict of form {exitcode num etc blah} #ie when the return result as a whole is treated as a command - #exitcode must be the first key + #exitcode must be the first key know {[lindex $args 0 0] eq "exitcode"} { uplevel 1 [list exitcode {*}[lrange [lindex $args 0] 1 end]] } @@ -5854,13 +5738,13 @@ namespace eval punk { #----------------------------- # - # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. - + # potentially can be disabled by config(?) - but then scripts not able to use all repl features.. + #todo - repl output info that it was evaluated as an expression #know {[expr $args] || 1} {expr $args} know {[expr $args] || 1} {tailcall expr $args} - #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc + #it is significantly faster to call a proc such as punk::lib::range like this than to inline it in the unknown proc #punk::lib::range is defined as a wrapper to lseq if it is available (8.7+) know {[regexp {^([+-]*[0-9_]+)\.\.([+-]*[0-9_]+)$} [lindex $args 0 0] -> from to]} {punk::lib::range $from $to} @@ -5879,18 +5763,18 @@ namespace eval punk { error "unknown_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $tail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs tail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs tail } #NOTE: - it doesn't make sense to call 'namespace' qualifiers or 'namespace tail' on a compound hd such as v,::etc= blah # we only look at leftmost namespace-like thing and need to take account of the pattern syntax - # e.g for ::etc,'::x'= + # e.g for ::etc,'::x'= # the ns is :: and the tail is etc,'::x'= # (Tcl's namespace qualifiers/tail won't help here) if {[string match ::* $hd]} { - set patterns [punk::_split_patterns_memoized $hd] + set patterns [punk::pipe::lib::_split_patterns_memoized $hd] #get a pair-list something like: {::x /0} {etc {}} set ns [namespace qualifiers [lindex $patterns 0 0]] set nslen [string length $ns] @@ -5904,27 +5788,27 @@ namespace eval punk { } else { set nscaller [uplevel 1 [list ::namespace current]] #jmn - set rhsmapped [pipecmd_namemapping $equalsrhs] + set rhsmapped [punk::pipe::lib::pipecmd_namemapping $equalsrhs] set commands [uplevel 1 [list ::info commands $pattern=$rhsmapped]] ;#uplevel - or else we are checking from perspective of this namespace ::punk #we must check for exact match of the command in the list - because command could have glob chars. if {"$pattern=$rhsmapped" in $commands} { puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'" #we call the namespaced function - we don't evaluate it *in* the namespace. #REVIEW - #warn for now...? + #warn for now...? #tailcall $pattern=$equalsrhs {*}$args tailcall $pattern=$rhsmapped {*}$tail } } #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" - #ignore the namespace.. + #ignore the namespace.. #We could interpret the fact that the nonexistant pipe was called with a namespace to indicate that's where the pipecommand should be created.. #But.. we would need to ensure 1st (compiling) invocation runs the same way as subsequent invocations. #namespace evaling match_assign here probably wouldn't accomplish that and may create surprises with regards to where lhs vars(if any) are created tailcall ::punk::match_assign $patterntail $equalsrhs {*}$tail #return [uplevel 1 [list ::punk::match_assign $varspecs $rhs $tail]] } - #variable re_assign {^([^\r\n=\{]*)=(.*)} + #variable re_assign {^([^\r\n=\{]*)=(.*)} #characters directly following = need to be assigned to the var even if they are escaped whitespace (e.g \t \r \n) #unescaped whitespace causes the remaining elements to be part of the tail -ie are appended to the var as a list #e.g x=a\nb c @@ -5992,12 +5876,12 @@ namespace eval punk { error "unknown_dot_assign_dispatch: pipeline with args unexpanded. Try {*}\$pipeline $argstail" } #regexp $punk::re_assign $hd _ pattern equalsrhs - #we assume the whole pipeline has been provided as the head + #we assume the whole pipeline has been provided as the head #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs - lassign [_rhs_tail_split $fullrhs] equalsrhs argstail + lassign [punk::pipe::lib::_rhs_tail_split $fullrhs] equalsrhs argstail } #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail @@ -6018,8 +5902,8 @@ namespace eval punk { know {[regexp {^([^=]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} #add escaping backslashes to a value - #matching odd keys in dicts using pipeline syntax can be tricky - as - #e.g + #matching odd keys in dicts using pipeline syntax can be tricky - as + #e.g #set ktest {a"b} #@@[escv $ktest].= list a"b val #without escv: @@ -6033,14 +5917,14 @@ namespace eval punk { #https://stackoverflow.com/questions/11135090/is-there-any-tcl-function-to-add-escape-character-automatically #thanks to DKF regsub -all {\W} $v {\\&} - } + } interp alias {} escv {} punk::escv #review #set v "\u2767" # #escv $v #\ - #the + #the #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { @@ -6048,17 +5932,17 @@ namespace eval punk { # #set tail [expr {($hd eq $partzerozero) ? $argstail : [concat [lrange $hd 1 end] $argstail ] }] ;#!WRONG. expr will convert some numbers to scientific notation - this is premature/undesirable! # #avoid using the return from expr and it works: # expr {($hd eq $partzerozero) ? [set tail $argstail] : [set tail [concat [lrange $hd 1 end] $argstail ]] } - # + # # tailcall ::punk::match_exec $varspecs $rhs {*}$tail # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] #} } - configure_unknown + configure_unknown #if client redefines 'unknown' after package require punk, they must call punk::configure_unknown afterwards. # - #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc + #main Pipe initiator function - needed especially if 'unknown' not configured to interpret x.= x= etc #Should theoretically be slightly faster.. but pipelines are relatively slow until we can get pipeline compiling and optimisation. proc % {args} { set arglist [lassign $args assign] ;#tail, head @@ -6068,12 +5952,12 @@ namespace eval punk { tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] } - set is_script [punk::arg_is_script_shaped $assign] + set is_script [punk::pipe::lib::arg_is_script_shaped $assign] if {!$is_script && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=\{]*)\.=$} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6092,7 +5976,7 @@ namespace eval punk { tailcall {*}$cmdlist - #result-based mismatch detection can probably never work nicely.. + #result-based mismatch detection can probably never work nicely.. #we need out-of-band method to detect mismatch. Otherwise we can't match on mismatch results! # set result [uplevel 1 $cmdlist] @@ -6128,10 +6012,10 @@ namespace eval punk { set cmdlist [list ::punk::pipeline .= "" "" {*}$arglist] } elseif {$assign eq "="} { set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} # set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} # set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { set cmdlist [list ::punk::pipeline .= $returnvarspecs "" {*}$arglist] @@ -6143,10 +6027,10 @@ namespace eval punk { } } else { set cmdlist $args - #script? + #script? #set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 $cmdlist} result erroptions]} { #puts stderr "pipematch erroptions:$erroptions" #debug.punk.pipe {pipematch error $result} 4 @@ -6236,7 +6120,7 @@ namespace eval punk { } } - #should only raise an error for pipe syntax errors - all other errors should be wrapped + #should only raise an error for pipe syntax errors - all other errors should be wrapped proc pipecase {args} { #debug.punk.pipe {pipecase level [info level] levelinfo [info level 0]} 9 set arglist [lassign $args assign] @@ -6245,10 +6129,10 @@ namespace eval punk { } elseif {$assign eq "="} { #set cmdlist [list ::punk::pipeline = "" "" {*}$arglist] set cmdlist [list ::= {*}$arglist] - } elseif {![punk::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { + } elseif {![punk::pipe::lib::arg_is_script_shaped $assign] && [string first "=" $assign] >= 0} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set dumbeditor {\}} - #set re_equals {^([^ \t\r\n=\{]*)=$} + #set re_equals {^([^ \t\r\n=\{]*)=$} #set dumbeditor {\}} if {[regexp {^([^ \t\r\n=]*)\.=.*} $assign _ returnvarspecs]} { @@ -6257,15 +6141,15 @@ namespace eval punk { set cmdlist [list $assign {*}$arglist] #set cmdlist [list ::punk::pipeline = $returnvarspecs "" {*}$arglist] } else { - error "pipesyntax pipecase unable to interpret pipeline '$args'" + error "pipesyntax pipecase unable to interpret pipeline '$args'" } #todo - account for insertion-specs e.g x=* x.=/0* } else { - #script? + #script? set cmdlist [list ::punk::pipeline .= "" "" {*}$args] } - + if {[catch {uplevel 1 [list ::if 1 $cmdlist]} result erroptions]} { #puts stderr "====>>> result: $result erroptions" set ecode [dict get $erroptions -errorcode] @@ -6308,14 +6192,14 @@ namespace eval punk { return [dict create error [dict create suppressed $result]] } default { - #normal tcl error + #normal tcl error #return [dict create error [dict create reason $result]] tailcall error $result "pipecase $args" [list caseerror] } } } } else { - tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] + tailcall return -errorcode [list casematch] [dict create ok [dict create result $result]] } } @@ -6329,7 +6213,7 @@ namespace eval punk { #unset args #upvar args upargs #set upargs $nextargs - upvar switchargs switchargs + upvar switchargs switchargs set switchargs $args uplevel 1 [::list ::if 1 $pipescript] } @@ -6339,7 +6223,7 @@ namespace eval punk { proc pipeswitchc {pipescript args} { set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list info vars] } else { set get_vars [list info locals] @@ -6377,13 +6261,13 @@ namespace eval punk { % - pipematch - ispipematch { incr i set e2 [lindex $args $i] - #set body [list $e {*}$e2] + #set body [list $e {*}$e2] #append body { $data} - - set body [list $e {*}$e2] + + set body [list $e {*}$e2] append body { {*}$data} - - + + set applylist [list {data} $body] #puts stderr $applylist set r [apply $applylist $r] @@ -6393,7 +6277,7 @@ namespace eval punk { incr i set e2 [lindex $args $i] set body [list $e $e2] - #pipeswitch takes 'args' - so expand $data when in pipedata context + #pipeswitch takes 'args' - so expand $data when in pipedata context append body { {*}$data} #use applylist instead of uplevel when in pipedata context! #can use either switchdata/data but not vars in calling context of 'pipedata' command. @@ -6421,8 +6305,7 @@ namespace eval punk { proc scriptlibpath {{shortname {}} args} { - upvar ::punk::config::running running_config - set scriptlib [dict get $running_config scriptlib] + set scriptlib [punk::config::configure running scriptlib] if {[string match "lib::*" $shortname]} { set relpath [string map [list "lib::" "" "::" "/"] $shortname] set relpath [string trimleft $relpath "/"] @@ -6452,7 +6335,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { set sep ";" } else { - # : ok for linux/bsd ... mac? + # : ok for linux/bsd ... mac? set sep ":" } set cond [string map [list $glob] {expr {[string length $item] && [string match $item]}}] @@ -6465,7 +6348,7 @@ namespace eval punk { } proc path {{glob *}} { set pipe [punk::path_list_pipe $glob] - {*}$pipe |> list_as_lines + {*}$pipe |> list_as_lines } #------------------------------------------------------------------- @@ -6508,7 +6391,7 @@ namespace eval punk { #e.g unix files such as /dev/null vs windows devices such as CON,PRN #e.g COM1 is mapped as /dev/ttyS1 in wsl (?) #Note also - tcl can have vfs mounted file which will appear as a directory to Tcl - but a file to external commands! - #We will stick with the Tcl view of the file system. + #We will stick with the Tcl view of the file system. #User can use their own direct calls to external utils if #Note we can't support $? directly in Tcl - script would have to test ${?} or use [set ?] proc sh_TEST {args} { @@ -6526,7 +6409,7 @@ namespace eval punk { if {$::tcl_platform(platform) eq "windows"} { #e.g trailing dot or trailing space if {[punk::winpath::illegalname_test $a2]} { - #protect with \\?\ to stop windows api from parsing + #protect with \\?\ to stop windows api from parsing #will do nothing if already prefixed with \\?\ set a2 [punk::winpath::illegalname_fix $a2] @@ -6536,7 +6419,7 @@ namespace eval punk { switch -- $a1 { -b { #dubious utility on FreeBSD, windows? - #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' + #FreeBSD has dropped support for block devices - stating 'No serious applications rely on block devices' #Linux apparently uses them though if{[file exists $a2]} { set boolresult [expr {[file type $a2] eq "blockSpecial"}] @@ -6545,7 +6428,7 @@ namespace eval punk { } } -c { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { set boolresult [expr {[file type $a2] eq "characterSpecial"}] } else { @@ -6559,9 +6442,9 @@ namespace eval punk { set boolresult [file exists $a2] } -f { - #e.g on windows CON,NUL + #e.g on windows CON,NUL if {[file exists $a2]} { - set boolresult [expr {[file type $a2] eq "file"}] + set boolresult [expr {[file type $a2] eq "file"}] } else { set boolresult false } @@ -6621,7 +6504,7 @@ namespace eval punk { } "-eq" { #test expects a possibly-large integer-like thing - #shell scripts will + #shell scripts will if {![is_sh_test_integer $a1]} { puts stderr "sh_TEST: invalid integer '$a1'" set lasterr 2 @@ -6725,7 +6608,7 @@ namespace eval punk { set exitcode [dict get $callinfo exitcode] if {[string length $errinfo]} { puts stderr "sh_TEST error in external call to 'test $args': $errinfo" - set lasterr $exitcode + set lasterr $exitcode } if {$exitcode == 0} { set boolresult true @@ -6761,7 +6644,7 @@ namespace eval punk { set c [lindex $args 0] if {[string is integer -strict $c]} { #return [expr {$c == 0}] - #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true + #return true/false to make it clearer we are outputting tcl-boolean inverse mapping from the shell style 0=true if {$c == 0} { return true } else { @@ -6801,7 +6684,7 @@ namespace eval punk { #maint - punk::args has similar #this is largely obsolete - uses dict for argspecs (defaults) instead of textblock as in punk::args #textblock has more flexibility in some ways - but not as easy to manipulate especially with regards to substitutions - #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? + #todo - consider a simple wrapper for punk::args to allow calling with dict of just name and default? #JMN #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. @@ -6857,7 +6740,7 @@ namespace eval punk { foreach {k v} $rawargs { if {![string match -* $k]} { break - } + } if {$i+1 >= [llength $rawargs]} { #no value for last flag error "bad options for $caller. No value supplied for last option $k" @@ -6957,7 +6840,7 @@ namespace eval punk { #NOT attempting to match haskell other than in overall concept. # - #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. + #magic var-names are a bit of a code-smell. But submitting only an expr argument is more Tcl-like than requiring an 'apply' specification. #Haskell seems to take an entire lambda so varnames can be user-specified - but the 'magic' there is in it's choice of submitting 2 elements at a time #We could do similar .. but we'll focus on comprehensibility for the basic cases - especially as begginning and end of list issues could be confusing. # @@ -7046,7 +6929,7 @@ namespace eval punk { } #group_numlist ? preserve representation of numbers rather than use string comparison? - + # - group_string #.= punk::group_string "aabcccdefff" @@ -7131,7 +7014,7 @@ namespace eval punk { #review #how do we stop matrix pipelines from leaving commands around? i.e how do we call destroy on the matrixchain wrapper if not explicitly? #Perhaps will be solved by: Tip 550: Garbage collection for TclOO - #Theoretically this should allow tidy up of objects created within the pipeline automatically + #Theoretically this should allow tidy up of objects created within the pipeline automatically #If the object name is placed in the pipeline variable dict then it should survive across segment apply scripts and only go out of scope at the end. proc matrix_command_from_rows {matrix_rows} { set mcmd [struct::matrix] @@ -7147,7 +7030,7 @@ namespace eval punk { set filtered_list [list] set binding {} if {[info level] == 1} { - #up 1 is global + #up 1 is global set get_vars [list ::info vars] } else { set get_vars [list ::info locals] @@ -7227,38 +7110,89 @@ namespace eval punk { return $linelist } - - #An implementation of a notoriously controversial metric. - proc LOC {args} { - set argspecs [subst { + namespace eval argdoc { + set DYN_ANTIGLOB_PATHS {${[punk::args::resolved_def -types opts ::punk::path::treefilenames -antiglob_paths]}} + punk::args::define { + @dynamic + @id -id ::punk::LOC + @cmd -name punk::LOC\ + -summary\ + "Lines Of Code counter"\ + -help\ + "LOC - lines of code. + An implementation of a notoriously controversial metric. + Returns a dict or dictionary-display containing various + counts such as: + 'loc' - total lines of code. + 'purepunctuationlines' - lines consisting soley of punctuation. + 'filecount' - number of files examined." + @opts + -return -default showdict -choices {dict showdict} -dir -default "\uFFFF" -exclude_dupfiles -default 1 -type boolean + ${$DYN_ANTIGLOB_PATHS} + -antiglob_files -default "" -type list -help\ + "Exclude if file tail matches any of these patterns" -exclude_punctlines -default 1 -type boolean + -show_largest -default 0 -type integer -help\ + "Report the top largest linecount files. + The value represents the number of files + to report on." + } " + #we could map away whitespace and use string is punct - but not as flexible? review -punctchars -default { [list \{ \} \" \\ - _ + = . > , < ' : \; ` ~ ! @ # \$ % ^ & * \[ \] ( ) | / ?] } - }] - set argd [punk::args::get_dict $argspecs $args] - lassign [dict values $argd] leaders opts vals - set searchspecs [dict values $vals] + " { + @values + fileglob -type string -default * -optional 1 -multiple 1 -help\ + "glob patterns to match against the filename portion (last segment) of each + file path. e.g *.tcl *.tm" + } + } + #An implementation of a notoriously controversial metric. + proc LOC {args} { + set argd [punk::args::parse $args withid ::punk::LOC] + lassign [dict values $argd] leaders opts values received + set searchspecs [dict get $values fileglob] - # -- --- --- --- --- --- - set opt_dir [dict get $opts -dir] + # -- --- --- --- --- --- + set opt_return [dict get $opts -return] + set opt_dir [dict get $opts -dir] if {$opt_dir eq "\uFFFF"} { set opt_dir [pwd] ;#pwd can take over a ms on windows in a not terribly deep path even with SSDs - so as a general rule we don't use it in the original defaults list } - # -- --- --- --- --- --- - set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] - set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars - set opt_punctchars [dict get $opts -punctchars] - # -- --- --- --- --- --- + # -- --- --- --- --- --- + set opt_exclude_dupfiles [dict get $opts -exclude_dupfiles] + set opt_exclude_punctlines [dict get $opts -exclude_punctlines] ;#exclude lines that consist purely of whitespace and the chars in -punctchars + set opt_punctchars [dict get $opts -punctchars] + set opt_largest [dict get $opts -show_largest] + set opt_antiglob_paths [dict get $opts -antiglob_paths] + set opt_antiglob_files [dict get $opts -antiglob_files] + # -- --- --- --- --- --- - set filepaths [punk::path::treefilenames -dir $opt_dir {*}$searchspecs] + set filepaths [punk::path::treefilenames -dir $opt_dir -antiglob_paths $opt_antiglob_paths -antiglob_files $opt_antiglob_files {*}$searchspecs] set loc 0 - set dupfileloc 0 - set seentails [list] + set dupfileloc 0 + set seentails [dict create] + set seencksums [dict create] ;#key is cksum value is list of paths + set largestloc [dict create] set dupfilecount 0 - set extensions [list] + set extensions [list] set purepunctlines 0 + set dupinfo [dict create] + set has_hashfunc [expr {![catch {package require sha1}]}] + set notes "" + if {$has_hashfunc} { + set dupfilemech sha1 + if {$opt_exclude_punctlines} { + append notes "checksums are on content stripped of whitespace lines,trailing whitespace, and pure punct lines. Does not indicate file contents equal.\n" + } else { + append notes "checksums are on content stripped of whitespace lines and trailing whitespace. Does not indicate file contents equal.\n" + } + } else { + set dupfilemech filetail + append notes "dupfilemech filetail because sha1 not loadable\n" + } foreach fpath $filepaths { set isdupfile 0 set floc 0 @@ -7267,111 +7201,318 @@ namespace eval punk { if {$ext ni $extensions} { lappend extensions $ext } + if {[catch {fcat $fpath} contents]} { + puts stderr "Error processing $fpath\n $contents" + continue + } + set lines [linelist -line {trimright} -block {trimall} $contents] if {!$opt_exclude_punctlines} { - set floc [llength [linelist -line {trimright} -block {trimall} [fcat $fpath]]] + set floc [llength $lines] + set comparedlines $lines } else { - set lines [linelist -line {trimright} -block {trimall} [fcat $fpath]] set mapawaypunctuation [list] foreach p $opt_punctchars empty {} { lappend mapawaypunctuation $p $empty } + set comparedlines [list] foreach ln $lines { if {[string length [string trim [string map $mapawaypunctuation $ln]]] > 0} { incr floc + lappend comparedlines $ln } else { incr fpurepunctlines - } + } } } - if {[file tail $fpath] in $seentails} { - set isdupfile 1 - incr dupfilecount - incr dupfileloc $floc + if {$opt_largest > 0} { + dict set largestloc $fpath $floc + } + if {$has_hashfunc} { + set cksum [sha1::sha1 [encoding convertto utf-8 [join $comparedlines \n]]] + if {[dict exists $seencksums $cksum]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + dict lappend seencksums $cksum $fpath + } else { + dict set seencksums $cksum [list $fpath] + } + } else { + if {[dict exists $seentails [file tail $fpath]]} { + set isdupfile 1 + incr dupfilecount + incr dupfileloc $floc + } } if {!$isdupfile || ($isdupfile && !$opt_exclude_dupfiles)} { incr loc $floc incr purepunctlines $fpurepunctlines } - lappend seentails [file tail $fpath] + dict lappend seentails [file tail $fpath] $fpath + #lappend seentails [file tail $fpath] + } + if {$has_hashfunc} { + dict for {cksum paths} $seencksums { + if {[llength $paths] > 1} { + dict set dupinfo checksums $cksum $paths + } + } + } + dict for {tail paths} $seentails { + if {[llength $paths] > 1} { + dict set dupinfo sametail $tail $paths + } } + if {$opt_exclude_punctlines} { - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions purepunctuationlines $purepunctlines] + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + purepunctuationlines $purepunctlines\ + notes $notes\ + ] + } else { + set result [dict create\ + loc $loc\ + filecount [llength $filepaths]\ + dupfiles $dupfilecount\ + dupfilemech $dupfilemech\ + dupfileloc $dupfileloc\ + dupinfo $dupinfo\ + extensions $extensions\ + notes $notes\ + ] + } + if {$opt_largest > 0} { + set largest_n [dict create] + set sorted [lsort -stride 2 -index 1 -decreasing -integer $largestloc] + set kidx 0 + for {set i 0} {$i < $opt_largest} {incr i} { + if {$kidx+1 > [llength $sorted]} {break} + dict set largest_n [lindex $sorted $kidx] [lindex $sorted $kidx+1] + incr kidx 2 + } + dict set result largest $largest_n + } + if {$opt_return eq "showdict"} { + return [punk::lib::showdict $result @@dupinfo/*/* !@@dupinfo] } - return [list loc $loc filecount [llength $filepaths] dupfiles $dupfilecount dupfileloc $dupfileloc extensions $extensions] + return $result } + ##dict of lists? + #a + # 1 + # 2 + #b + # 3 + # 4 + # "" + # etc + # d + # D + # "ok then" + + + ##dict of dicts + #a + # x + # 1 + # y + # 2 + #b + # x + # 11 + + ##dict of mixed + #list + # a + # b + # c + #dict + # a + # aa + # b + # bb + #val + # x + #list + # a + # b + + # each line has 1 key or value OR part of 1 key or value. ie <=1 key/val per line! + ##multiline + #key + # "multi + # line value" + # + #-------------------------------- + #a + # 1 + # 2 + + #vs + + #a + # 1 + # 2 + + #dict of list-len 2 is equiv to dict of dict with one keyval pair + #-------------------------------- + + - #!!!todo fix - linedict is unfinished and non-functioning - #linedict based on indents + #!!!todo fix - linedict is unfinished and non-functioning + #linedict based on indents proc linedict {args} { + puts stderr "linedict is experimental and incomplete" set data [lindex $args 0] - set opts [lrange $args 1 end] ;#todo + set opts [lrange $args 1 end] ;#todo set nlsplit [split $data \n] set rootindent -1 set stepindent -1 - #set wordlike_parts [regexp -inline -all {\S+} $lastitem] - set d [dict create] - set keys [list] - set i 1 - set firstkeyline "N/A" - set firststepline "N/A" + + #first do a partial loop through lines and work out the rootindent and stepindent. + #we could do this in the main loop - but we do it here to remove a small bit of logic from the main loop. + #review - if we ever move to streaming a linedict - we'll need to re-arrange to validating indents as we go anyway. + set linenum 0 + set firstkey_line "N/A" + set firstkey_linenum -1 + set firststep_line "N/A" + set firststep_linenum -1 + set indents_seen [dict create] foreach ln $nlsplit { + incr linenum if {![string length [string trim $ln]]} { - incr i continue } - set is_rootkey 0 + + #todo - use info complete to accept keys/values with newlines regexp {(\s*)(.*)} $ln _ space linedata - puts stderr ">>line:'$ln' [string length $space] $linedata" - set this_indent [string length $space] - if {$rootindent < 0} { - set firstkeyline $ln - set rootindent $this_indent + if {[catch {lindex $linedata 0}]} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent == $rootindent} { - set is_rootkey 1 + if {[llength $linedata] > 1} { + error "bad line value '$linedata' - each line must be a valid Tcl list of length 1. Use quotes or curly braces as necessary" } - if {$this_indent < $rootindent} { - error "bad root indentation ($this_indent) at line: $i smallest indent was set by first key line: $firstkeyline" + #puts stderr "--linenum:[format %-3s $linenum] line:[format "%-20s" $ln] [format %-4s [string length $space]] $linedata" + set this_indent [string length $space] + if {[dict exists $indents_seen $this_indent]} { + continue } - if {$is_rootkey} { - dict set d $linedata {} - lappend keys $linedata + if {$rootindent < 0} { + set firstkey_line $ln + set firstkey_linenum $linenum + set rootindent $this_indent + dict set indents_seen $this_indent 1 + } elseif {$stepindent < 0} { + if {$this_indent > $rootindent} { + set firststep_line $ln + set firststep_linenum $linenum + set stepindent [expr {$this_indent - $rootindent}] + dict set indents_seen $this_indent 1 + } elseif {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" + } + #if equal - it's just another root key } else { - if {$stepindent < 0} { - set stepindent $this_indent - set firststepline $ln + #validate all others + if {$this_indent < $rootindent} { + error "bad root indentation ($this_indent) at line: $linenum. Smallest indent was set on linenumber: $firstkey_linenum by first key line: $firstkey_line" } - if {$this_indent == $stepindent} { - dict set d [lindex $keys end] $ln + if {($this_indent - $rootindent) % $stepindent != 0} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. this_indent - rootindent ($this_indent - $rootindent == [expr {$this_indent - $rootindent}]) is not a multiple of the first key indent $stepindent seen on linenumber: $firststep_linenum value:'$firststep_line'" } else { - if {($this_indent % $stepindent) != 0} { - error "bad indentation ($this_indent) at line: $i not a multiple of the first key indent $step_indent seen on $firststepline" - } + dict set indents_seen $this_indent 1 + } + } + } + - #todo fix! + #set wordlike_parts [regexp -inline -all {\S+} $lastitem] + set d [dict create] + set keys [list] + set linenum 0 ;#line-numbers 1 based + foreach ln $nlsplit { + incr linenum + if {![string length [string trim $ln]]} { + incr linenum + continue + } + regexp {(\s*)(.*)} $ln _ space linedata + puts stderr ">>linenum:[format %-3s $linenum] line:[format "%-20s " $ln] [format %-4s [string length $space]] $linedata" + set linedata [lindex $linedata 0] + set this_indent [string length $space] + + + if {$this_indent == $rootindent} { + #is rootkey + dict set d $linedata {} + set keys [list $linedata] + } else { + set ispan [expr {$this_indent - $rootindent}] + set numsteps [expr {$ispan / $stepindent}] + #assert - since validated in initial loop - numsteps is always >= 1 + set keydepth [llength $keys] + if {$numsteps > $keydepth + 1} { + #too deep - not tested for in initial loop. ? todo - convert to leading spaces in key/val? + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + if {$numsteps > ($keydepth - 1)} { + #assert - from above test - must be 1 or 2 deeper set parentkey [lindex $keys end] - lappend keys [list $parentkey $ln] - set oldval [dict get $d $parentkey] - if {[string length $oldval]} { - set new [dict create $oldval $ln] + set oldval [dict get $d {*}$parentkey] + if {$numsteps - ($keydepth -1) == 1} { + #1 deeper + if {$oldval ne {}} { + lappend keys [list {*}$parentkey $linedata] + dict unset d {*}$parentkey + #dict set d {*}$parentkey $oldval $linedata + dict set d {*}$parentkey $oldval {} ;#convert to key? + dict set d {*}$parentkey $linedata {} + } else { + dict set d {*}$parentkey $linedata + } } else { - dict set d $parentkey $ln - } - + #2 deeper - only ok if there is an existing val + if {$oldval eq {}} { + error "bad indentation ($this_indent) at linenum: $linenum line:'$ln'. (too deep) - review" + } + puts ">>> 2deep d:'$d' oldval:$oldval linedata:$linedata parentkey:$parentkey" + dict unset d {*}$parentkey + dict set d {*}$parentkey $oldval $linedata + lappend keys [list {*}$parentkey $oldval] + } + } elseif {$numsteps < ($keydepth - 1)} { + set diff [expr {$keydepth - 1 - $numsteps}] + set keys [lrange $keys 0 end-$diff] + #now treat as same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} + } else { + #same level + set parentkey [lindex $keys end-1] + lset keys end end $linedata + dict set d {*}$parentkey $linedata {} } } - incr i + #puts ">>keys:$keys" } return $d } - proc dictline {d} { + proc dictline {d {indent 2}} { puts stderr "unimplemented" set lines [list] - + return $lines } @@ -7414,79 +7555,79 @@ namespace eval punk { @id -id ::punk::inspect @cmd -name punk::inspect -help\ "Function to display values - used pimarily in a punk pipeline. - The raw value arguments (not options) are always returned to pass - forward in the pipeline. - (pipeline data inserted at end of each |...> segment is passed as single item unless - inserted with an expanding insertion specifier such as .=>* ) - e.g1: - .= list a b c |v1,/1-end,/0>\\ - .=>* inspect -label i1 -- |>\\ - .=v1> inspect -label i2 -- |>\\ - string toupper - (3) i1: {a b c} {b c} a - (1) i2: a b c - - - A B C - " + The raw value arguments (not options) are always returned to pass + forward in the pipeline. + (pipeline data inserted at end of each |...> segment is passed as single item unless + inserted with an expanding insertion specifier such as .=>* ) + e.g1: + .= list a b c |v1,/1-end,/0>\\ + .=>* inspect -label i1 -- |>\\ + .=v1> inspect -label i2 -- |>\\ + string toupper + (3) i1: {a b c} {b c} a + (1) i2: a b c + + - A B C + " -label -type string -default "" -help\ "An optional label to help distinguish output when multiple - inspect statements are in a pipeline. This appears after the - bracketed count indicating number of values supplied. - e.g (2) MYLABEL: val1 val2 - The label can include ANSI codes. - e.g - inspect -label [a+ red]mylabel -- val1 val2 val3 - " + inspect statements are in a pipeline. This appears after the + bracketed count indicating number of values supplied. + e.g (2) MYLABEL: val1 val2 + The label can include ANSI codes. + e.g + inspect -label [a+ red]mylabel -- val1 val2 val3 + " -limit -type int -default 20 -help\ "When multiple values are passed to inspect - limit the number - of elements displayed in -channel output. - When truncation has occured an elipsis indication (...) will be appended. - e.g - .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ - (11) 20 23 26 29... + of elements displayed in -channel output. + When truncation has occured an elipsis indication (...) will be appended. + e.g + .= lseq 20 to 50 by 3 |> .=>* inspect -limit 4 -- |> .=>* tcl::mathop::+ + (11) 20 23 26 29... - - 385 + - 385 - For no limit - use -limit -1 - " + For no limit - use -limit -1 + " -channel -type string -default stderr -help\ "An existing open channel to write to. If value is any of nul, null, /dev/nul - the channel output is disabled. This effectively disables inspect as the args - are simply passed through in the return to continue the pipeline. - " + the channel output is disabled. This effectively disables inspect as the args + are simply passed through in the return to continue the pipeline. + " -showcount -type boolean -default 1 -help\ "Display a leading indicator in brackets showing the number of arg values present." -ansi -type integer -default 1 -nocase 1 -choices {0 1 2 VIEW 3 VIEWCODES 4 VIEWSTYLES} -choicelabels { 0 "Strip ANSI codes from display - of values. The disply output will - still be colourised if -ansibase has - not been set to empty string or - [a+ normal]. The stderr or stdout - channels may also have an ansi colour. - (see 'colour off' or punk::config)" + of values. The disply output will + still be colourised if -ansibase has + not been set to empty string or + [a+ normal]. The stderr or stdout + channels may also have an ansi colour. + (see 'colour off' or punk::config)" 1 "Leave value as is" 2 "Display the ANSI codes and - other control characters inline - with replacement indicators. - e.g esc, newline, space, tab" + other control characters inline + with replacement indicators. + e.g esc, newline, space, tab" VIEW "Alias for 2" - 3 "Display as per 2 but with - colourised ANSI replacement codes." + 3 "Display as per 2 but with + colourised ANSI replacement codes." VIEWCODES "Alias for 3" 4 "Display ANSI and control - chars in default colour, but - apply the contained ansi to - the text portions so they display - as they would for -ansi 1" - VIEWSTYLE "Alias for 4" - } + chars in default colour, but + apply the contained ansi to + the text portions so they display + as they would for -ansi 1" + VIEWSTYLE "Alias for 4" + } -ansibase -type ansistring -default {${[a+ brightgreen]}} -help\ "Base ansi code(s) that will apply to output written to the chosen -channel. - If there are ansi resets in the displayed values - output will revert to this base. - Does not affect return value." + If there are ansi resets in the displayed values - output will revert to this base. + Does not affect return value." -- -type none -help\ "End of options marker. - It is advisable to use this, as data in a pipeline may often begin with -" + It is advisable to use this, as data in a pipeline may often begin with -" @values -min 0 -max -1 arg -type string -optional 1 -multiple 1 -help\ @@ -7500,7 +7641,7 @@ namespace eval punk { set flags [list] set endoptsposn [lsearch $args --] ;#first -- if data expected to contain --, then should always be called with --. e.g inspect -- if {$endoptsposn >= 0} { - set flags [lrange $args 0 $endoptsposn-1] + set flags [lrange $args 0 $endoptsposn-1] set pipeargs [lrange $args $endoptsposn+1 end] } else { #no explicit end of opts marker @@ -7551,7 +7692,7 @@ namespace eval punk { set val [lindex $pipeargs 0] set count 1 } else { - #but the pipeline segment could have an insertion-pattern ending in * + #but the pipeline segment could have an insertion-pattern ending in * set val $pipeargs set count [llength $pipeargs] } @@ -7597,7 +7738,7 @@ namespace eval punk { set ansibase [dict get $opts -ansibase] if {$ansibase ne ""} { - #-ansibase default is hardcoded into punk::args definition + #-ansibase default is hardcoded into punk::args definition #run a test using any ansi code to see if colour is still enabled if {[a+ red] eq ""} { set ansibase "" ;#colour seems to be disabled @@ -7609,27 +7750,31 @@ namespace eval punk { set displayval $ansibase[punk::ansi::ansistrip $displayval] } 1 { - #val may have ansi - including resets. Pass through ansibase_lines to + #val may have ansi - including resets. Pass through ansibase_lines to if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 2 { set displayval $ansibase[ansistring VIEW $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 3 { set displayval $ansibase[ansistring VIEWCODE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } 4 { set displayval $ansibase[ansistring VIEWSTYLE $displayval] if {$ansibase ne ""} { - set displayval [::textblock::ansibase_lines $displayval $ansibase] + #set displayval [::textblock::ansibase_lines $displayval $ansibase] + set displayval [punk::ansi::ansiwrap_raw $ansibase "" "" $displayval] } } } @@ -7665,6 +7810,7 @@ namespace eval punk { set cmdinfo [list] lappend cmdinfo [list help "?topics?" "This help. To see available subitems type: help topics"] lappend cmdinfo [list i "cmd ?subcommand...?" "Show usage for a command or ensemble subcommand"] + lappend cmdinfo [list s "cmd ?subcommand...?" "Show synopsis for a command or ensemble subcommand"] lappend cmdinfo [list ./ "?subdir?" "view/change directory"] lappend cmdinfo [list ../ "" "go up one directory"] lappend cmdinfo [list ./new "subdir" "make new directory and switch to it"] @@ -7692,9 +7838,9 @@ namespace eval punk { $t configure_column 1 -minwidth [expr {$width_1 + 1}] $t configure -title $title - set text "" + set text "" append text [$t print] - + set warningblock "" set introblock $mascotblock @@ -7743,14 +7889,14 @@ namespace eval punk { upvar ::punk::config::other_env_vars_config otherenv_config set known_punk [dict keys $punkenv_config] - set known_other [dict keys $otherenv_config] + set known_other [dict keys $otherenv_config] append text \n set usetable 1 if {$usetable} { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] if {"windows" eq $::tcl_platform(platform)} { #If any env vars have been set to empty string - this is considered a deletion of the variable on windows. - #The Tcl ::env array is linked to the underlying process view of the environment + #The Tcl ::env array is linked to the underlying process view of the environment #- but info exists ::env(var) can misreport as true if it has been deleted by setting to empty string rather than using unset. #an 'array get' will resynchronise. #Even if an env variable didn't exist before - setting it to empty string can get it to this inconsistent state. @@ -7759,7 +7905,7 @@ namespace eval punk { #do an array read on ::env foreach {v vinfo} $punkenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7778,7 +7924,7 @@ namespace eval punk { set t [textblock::class::table new -show_hseps 0 -show_header 1 -ansiborder_header [a+ web-green]] foreach {v vinfo} $otherenv_config { if {[info exists ::env($v)]} { - set c2 [set ::env($v)] + set c2 [set ::env($v)] } else { set c2 "(NOT SET)" } @@ -7795,12 +7941,12 @@ namespace eval punk { append text $linesep\n append text "punk environment vars:\n" append text $linesep\n - set col1 [string repeat " " 25] + set col1 [string repeat " " 25] set col2 [string repeat " " 50] foreach v $known_punk { set c1 [overtype::left $col1 $v] if {[info exists ::env($v)]} { - set c2 [overtype::left $col2 [set ::env($v)] + set c2 [overtype::left $col2 [set ::env($v)]] } else { set c2 [overtype::right $col2 "(NOT SET)"] } @@ -7816,27 +7962,33 @@ namespace eval punk { set indent [string repeat " " [string length "WARNING: "]] lappend cstring_tests [dict create\ type "PM "\ - msg "PRIVACY MESSAGE"\ + msg "UN"\ f7 punk::ansi::controlstring_PM\ - f7desc "7bit ESC ^"\ + f7prefix "7bit ESC ^ secret "\ + f7suffix "safe"\ f8 punk::ansi::controlstring_PM8\ - f8desc "8bit \\x9e"\ + f8prefix "8bit \\x9e secret "\ + f8suffix "safe"\ ] lappend cstring_tests [dict create\ type SOS\ - msg "STRING"\ + msg "NOT"\ f7 punk::ansi::controlstring_SOS\ - f7desc "7bit ESC X"\ + f7prefix "7bit ESC X string "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_SOS8\ - f8desc "8bit \\x98"\ + f8prefix "8bit \\x98 string "\ + f8suffix " hidden"\ ] lappend cstring_tests [dict create\ type APC\ - msg "APPLICATION PROGRAM COMMAND"\ + msg "NOT"\ f7 punk::ansi::controlstring_APC\ - f7desc "7bit ESC _"\ + f7prefix "7bit ESC _ APPLICATION PROGRAM COMMAND "\ + f7suffix " hidden"\ f8 punk::ansi::controlstring_APC8\ - f8desc "8bit \\x9f"\ + f8prefix "8bit \\x9f APPLICATION PROGRAM COMMAND "\ + f8suffix " hidden"\ ] foreach test $cstring_tests { @@ -7846,14 +7998,14 @@ namespace eval punk { set hidden_width_m8 [punk::console::test_char_width $m8] if {$hidden_width_m != 0 || $hidden_width_m8 != 0} { if {$hidden_width_m == 0} { - set d "[a+ green bold][dict get $test f7desc] [a red]${m}[a]" + set d "[a+ green bold][dict get $test f7prefix][a red]${m}[a][a+ green bold][dict get $test f7suffix][a]" } else { - set d "[a+ yellow bold][dict get $test f7desc] [a red]$m[a]" + set d "[a+ yellow bold][dict get $test f7prefix][a red]$m[a][a+ yellow bold][dict get $test f7suffix][a]" } if {$hidden_width_m8 == 0} { - set d8 "[a+ green ][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ green ][dict get $test f8prefix][a red]$m8[a][a+ green][dict get $test f8suffix][a]" } else { - set d8 "[a+ yellow bold][dict get $test f8desc] [a red]$m8[a]" + set d8 "[a+ yellow bold][dict get $test f8prefix][a red]$m8[a][a+ yellow bold][dict get $test f8suffix][a]" } append warningblock \n "WARNING: terminal doesn't hide all [dict get $test type] control strings: $d $d8" } @@ -7923,7 +8075,7 @@ namespace eval punk { } set widest0 [$t column_datawidth 0] $t configure_column 0 -minwidth [expr {$widest0 + 4}] - append text \n[$t print] + append text \n[$t print] lappend chunks [list stdout $text] } @@ -7933,7 +8085,7 @@ namespace eval punk { proc help {args} { set chunks [help_chunks {*}$args] foreach chunk $chunks { - lassign $chunk chan text + lassign $chunk chan text puts -nonewline $chan $text } } @@ -7963,8 +8115,7 @@ namespace eval punk { interp alias {} know {} punk::know interp alias {} know? {} punk::know? - #interp alias {} arg {} punk::val - interp alias {} val {} punk::val + #interp alias {} val {} punk::val interp alias {} exitcode {} punk::exitcode interp alias {} hide {} punkapp::hide_console ;#will only work if controllable toplevels exist @@ -7979,7 +8130,7 @@ namespace eval punk { - + #friendly sh aliases (which user may wish to disable e.g if conflicts) interp alias {} test {} punk::sh_test ;#not much reason to run 'test' directly in punk shell (or tclsh shell) as returncode not obvious anyway due to use of exec interp alias {} TEST {} punk::sh_TEST; #double-evaluation to return tcl true/false from exitcode @@ -8016,7 +8167,7 @@ namespace eval punk { #---------------------------------------------- interp alias {} linelistraw {} punk::linelistraw - + # 'path' collides with kettle path in kettle::doc function - todo - patch kettle? interp alias {} PATH {} punk::path @@ -8066,13 +8217,13 @@ namespace eval punk { # ls aliases - note that tcl doesn't exand * but sh_xxx functions pass to sh -c allowing shell expansion interp alias {} l {} sh_runout -n ls -A ;#plain text listing - #interp alias {} ls {} sh_runout -n ls -AF --color=always + #interp alias {} ls {} sh_runout -n ls -AF --color=always interp alias {} ls {} shellrun::runconsole ls -AF --color=always ;#use unknown to use terminal and allow | more | less #note that shell globbing with * won't work on unix systems when using unknown/exec interp alias {} lw {} sh_runout -n ls -AFC --color=always ;#wide listing (use A becaus no extra info on . & ..) interp alias {} ll {} sh_runout -n ls -laFo --color=always ;#use a instead of A to see perms/owner of . & .. # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases? - #interp alias {} lw {} ls -aFv --color=always + #interp alias {} lw {} ls -aFv --color=always interp alias {} dir {} shellrun::runconsole dir @@ -8093,7 +8244,7 @@ namespace eval punk { interp alias {} ./~ {} punk::nav::fs::d/~ interp alias {} d/~ {} punk::nav::fs::d/~ interp alias "" x/ "" punk::nav::fs::x/ - + if {$::tcl_platform(platform) eq "windows"} { set has_powershell 1 @@ -8101,10 +8252,10 @@ namespace eval punk { interp alias {} dw {} dir /W/D } else { #todo - natsorted equivalent - #interp alias {} dl {} + #interp alias {} dl {} interp alias {} dl {} puts stderr "not implemented" interp alias {} dw {} puts stderr "not implemented" - #todo - powershell detection on other platforms + #todo - powershell detection on other platforms set has_powershell 0 } if {$has_powershell} { @@ -8142,7 +8293,7 @@ namespace eval punk { if {[punk::repl::codethread::is_running]} { puts stdout "Attempting repl stop. Try ctrl-c or exit command to leave interpreter" set ::repl::done 1 - } + } } start { if {[punk::repl::codethread::is_running]} { @@ -8167,8 +8318,8 @@ punk::mod::cli set_alias app #todo - change to punk::dev package require punk::mix -punk::mix::cli set_alias dev -punk::mix::cli set_alias deck ;#deprecate! +punk::mix::cli set_alias dev +punk::mix::cli set_alias deck ;#deprecate! #todo - add punk::deck for managing cli modules and commandsets diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm index fd638812..b8fada0b 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/aliascore-0.1.0.tm @@ -118,6 +118,7 @@ tcl::namespace::eval punk::aliascore { pdict ::punk::lib::pdict\ plist {::punk::lib::pdict -roottype list}\ showlist {::punk::lib::showdict -roottype list}\ + grepstr ::punk::grepstr\ rehash ::punk::rehash\ showdict ::punk::lib::showdict\ ansistrip ::punk::ansi::ansistrip\ @@ -136,6 +137,7 @@ tcl::namespace::eval punk::aliascore { rmcup ::punk::console::disable_alt_screen\ config ::punk::config\ s ::punk::ns::synopsis\ + eg ::punk::ns::eg\ ] #*** !doctools diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm index b8d172da..6b04827d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi-0.1.1.tm @@ -611,7 +611,7 @@ tcl::namespace::eval punk::ansi { } ""] proc example {args} { - set argd [punk::args::get_by_id ::punk::ansi::example $args] + set argd [punk::args::parse $args withid ::punk::ansi::example] set colwidth [dict get $argd opts -colwidth] if {[info commands file] eq ""} { error "file command unavailable - punk::ansi::example cannot be shown" @@ -723,7 +723,8 @@ tcl::namespace::eval punk::ansi { } lappend adjusted_row $i } - append result [textblock::join_basic -- {*}$adjusted_row] \n + #append result [textblock::join_basic -- {*}$adjusted_row] \n + append result [textblock::join_basic_raw {*}$adjusted_row] \n incr rowindex } @@ -876,6 +877,7 @@ tcl::namespace::eval punk::ansi { tlc l\ trc k\ blc m\ + brc j\ ltj t\ rtj u\ ttj w\ @@ -985,51 +987,51 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #css 1-2.0 HTML 3.2-4 Basic colours eg web-silver for fg Web-silver for bg # variable WEB_colour_map_basic - tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF - tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 - tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 - tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 - tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 - tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 - tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 - tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 - tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 - tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF - tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 - tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF - tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 - tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF - tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_basic white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_basic silver 192-192-192 ;# #C0C0C0 + tcl::dict::set WEB_colour_map_basic gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_basic black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_basic red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_basic maroon 128-0-0 ;# #800000 + tcl::dict::set WEB_colour_map_basic yellow 255-255-0 ;# #FFFF00 + tcl::dict::set WEB_colour_map_basic olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_basic lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_basic green 0-128-0 ;# #008000 + tcl::dict::set WEB_colour_map_basic aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_basic teal 0-128-128 ;# #008080 + tcl::dict::set WEB_colour_map_basic blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_basic navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_basic fuchsia 255-0-255 ;# #FF00FF + tcl::dict::set WEB_colour_map_basic purple 128-0-128 ;# #800080 # -- --- --- #Pink colours variable WEB_colour_map_pink - tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 - tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 - tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 - tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 - tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 - tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB + tcl::dict::set WEB_colour_map_pink mediumvioletred 199-21-133 ;# #C71585 + tcl::dict::set WEB_colour_map_pink deeppink 255-20-147 ;# #FF1493 + tcl::dict::set WEB_colour_map_pink palevioletred 219-112-147 ;# #DB7093 + tcl::dict::set WEB_colour_map_pink hotpink 255-105-180 ;# #FF69B4 + tcl::dict::set WEB_colour_map_pink lightpink 255-182-193 ;# #FFB6C1 + tcl::dict::set WEB_colour_map_pink pink 255-192-203 ;# #FFCOCB # -- --- --- #Red colours variable WEB_colour_map_red - tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 - tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 - tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 - tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C - tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C - tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 - tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 - tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A - tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A + tcl::dict::set WEB_colour_map_red darkred 139-0-0 ;# #8B0000 + tcl::dict::set WEB_colour_map_red red 255-0-0 ;# #FF0000 + tcl::dict::set WEB_colour_map_red firebrick 178-34-34 ;# #B22222 + tcl::dict::set WEB_colour_map_red crimson 220-20-60 ;# #DC143C + tcl::dict::set WEB_colour_map_red indianred 205-92-92 ;# #CD5C5C + tcl::dict::set WEB_colour_map_red lightcoral 240-128-128 ;# #F08080 + tcl::dict::set WEB_colour_map_red salmon 250-128-114 ;# #FA8072 + tcl::dict::set WEB_colour_map_red darksalmon 233-150-122 ;# #E9967A + tcl::dict::set WEB_colour_map_red lightsalmon 255-160-122 ;# #FFA07A # -- --- --- #Orange colours variable WEB_colour_map_orange - tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 - tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 - tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 - tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 - tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 + tcl::dict::set WEB_colour_map_orange orangered 255-69-0 ;# #FF4500 + tcl::dict::set WEB_colour_map_orange tomato 255-99-71 ;# #FF6347 + tcl::dict::set WEB_colour_map_orange darkorange 255-140-0 ;# #FF8C00 + tcl::dict::set WEB_colour_map_orange coral 255-127-80 ;# #FF7F50 + tcl::dict::set WEB_colour_map_orange orange 255-165-0 ;# #FFA500 # -- --- --- #Yellow colours variable WEB_colour_map_yellow @@ -1041,7 +1043,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_yellow palegoldenrod 238-232-170 ;# #EEE8AA tcl::dict::set WEB_colour_map_yellow moccasin 255-228-181 ;# #FFE4B5 tcl::dict::set WEB_colour_map_yellow papayawhip 255-239-213 ;# #FFEFD5 - tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 + tcl::dict::set WEB_colour_map_yellow lightgoldenrodyeallow 250-250-210 ;# #FAFAD2 tcl::dict::set WEB_colour_map_yellow lemonchiffon 255-250-205 ;# #FFFACD tcl::dict::set WEB_colour_map_yellow lightyellow 255-255-224 ;# #FFFFE0 # -- --- --- @@ -1068,7 +1070,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Purple, violet, and magenta colours variable WEB_colour_map_purple tcl::dict::set WEB_colour_map_purple indigo 75-0-130 ;# #4B0082 - tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 + tcl::dict::set WEB_colour_map_purple purple 128-0-128 ;# #800080 tcl::dict::set WEB_colour_map_purple darkmagenta 139-0-139 ;# #8B008B tcl::dict::set WEB_colour_map_purple darkviolet 148-0-211 ;# #9400D3 tcl::dict::set WEB_colour_map_purple darkslateblue 72-61-139 ;# #9400D3 @@ -1089,10 +1091,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #Blue colours variable WEB_colour_map_blue tcl::dict::set WEB_colour_map_blue midnightblue 25-25-112 ;# #191970 - tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 + tcl::dict::set WEB_colour_map_blue navy 0-0-128 ;# #000080 tcl::dict::set WEB_colour_map_blue darkblue 0-0-139 ;# #00008B tcl::dict::set WEB_colour_map_blue mediumblue 0-0-205 ;# #0000CD - tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF + tcl::dict::set WEB_colour_map_blue blue 0-0-255 ;# #0000FF tcl::dict::set WEB_colour_map_blue royalblue 65-105-225 ;# #4169E1 tcl::dict::set WEB_colour_map_blue steelblue 70-130-180 ;# #4682B4 tcl::dict::set WEB_colour_map_blue dodgerblue 30-144-255 ;# #1E90FF @@ -1113,7 +1115,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_cyan darkturquoise 0-206-209 ;# #00CED1 tcl::dict::set WEB_colour_map_cyan mediumturquoise 72-209-204 ;# #48D1CC tcl::dict::set WEB_colour_map_cyan turquoise 64-224-208 ;# #40E0D0 - tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF + tcl::dict::set WEB_colour_map_cyan aqua 0-255-255 ;# #00FFFF tcl::dict::set WEB_colour_map_cyan cyan 0-255-255 ;# #00FFFF - same as aqua tcl::dict::set WEB_colour_map_cyan aquamarine 127-255-212 ;# #7FFFD4 tcl::dict::set WEB_colour_map_cyan paleturquoise 175-238-238 ;# #AFEEEE @@ -1126,11 +1128,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_green darkolivegreen 85-107-47 ;# #55682F tcl::dict::set WEB_colour_map_green forestgreen 34-139-34 ;# #228B22 tcl::dict::set WEB_colour_map_green seagreen 46-139-87 ;# #2E8B57 - tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 + tcl::dict::set WEB_colour_map_green olive 128-128-0 ;# #808000 tcl::dict::set WEB_colour_map_green olivedrab 107-142-35 ;# #6B8E23 tcl::dict::set WEB_colour_map_green mediumseagreen 60-179-113 ;# #3CB371 tcl::dict::set WEB_colour_map_green limegreen 50-205-50 ;# #32CD32 - tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 + tcl::dict::set WEB_colour_map_green lime 0-255-0 ;# #00FF00 tcl::dict::set WEB_colour_map_green springgreen 0-255-127 ;# #00FF7F tcl::dict::set WEB_colour_map_green mediumspringgreen 0-250-154 ;# #00FA9A tcl::dict::set WEB_colour_map_green darkseagreen 143-188-143 ;# #8FBC8F @@ -1160,15 +1162,15 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu tcl::dict::set WEB_colour_map_white mintcream 245-255-250 ;# #F5FFFA tcl::dict::set WEB_colour_map_white snow 255-250-250 ;# #FFFAFA tcl::dict::set WEB_colour_map_white ivory 255-255-240 ;# #FFFFF0 - tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF + tcl::dict::set WEB_colour_map_white white 255-255-255 ;# #FFFFFF # -- --- --- #Gray and black colours variable WEB_colour_map_gray - tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 + tcl::dict::set WEB_colour_map_gray black 0-0-0 ;# #000000 tcl::dict::set WEB_colour_map_gray darkslategray 47-79-79 ;# #2F4F4F tcl::dict::set WEB_colour_map_gray dimgray 105-105-105 ;# #696969 tcl::dict::set WEB_colour_map_gray slategray 112-128-144 ;# #708090 - tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 + tcl::dict::set WEB_colour_map_gray gray 128-128-128 ;# #808080 tcl::dict::set WEB_colour_map_gray lightslategray 119-136-153 ;# #778899 tcl::dict::set WEB_colour_map_gray darkgray 169-169-169 ;# #A9A9A9 tcl::dict::set WEB_colour_map_gray silver 192-192-192 ;# #C0C0C0 @@ -1201,6 +1203,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set X11_colour_map [tcl::dict::merge $WEB_colour_map $X11_colour_map_diff] + + + + #Xterm colour names (256 colours) #lists on web have duplicate names #these have been renamed here in a systematic way: @@ -1217,6 +1223,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #The xterm names are boringly unimaginative - and also have some oddities such as: # DarkSlateGray1 which looks much more like cyan.. # The greyxx names are spelt with an e - but the darkslategrayX variants use an a. Perhaps that's because they are more cyan than grey and the a is a hint? + #(more likely just a mix of UK vs US spelling) # there is no gold or gold2 - but there is gold1 and gold3 #but in general the names bear some resemblance to the colours and are at least somewhat intuitive. @@ -1612,7 +1619,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set fg "black" } } - lappend clist "[a+ {*}$fc {*}$fg Term$i][format %3s $i]" + lappend clist "[a+ {*}$fc {*}$fg Term-$i][format %3s $i]" } set t [textblock::list_as_table -columns 36 -return tableobject $clist] @@ -1636,7 +1643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 8} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] } @@ -1698,7 +1705,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { set br "" } - append out "$br[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "$br[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } append out [a] return [tcl::string::trimleft $out \n] @@ -1723,7 +1730,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_seps 0 -show_edge 0 for {set i 16} {$i <=231} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== $cols} { lappend rows $row set row [list] @@ -1792,7 +1799,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu foreach cnum $pastel8 { append p8 "[a+ {*}$fc $fg Term-$cnum][format %3s $cnum] " } - append p8 [a]\n + #append p8 [a]\n + #append out \n $p8 + + append p8 [a] append out \n $p8 return $out @@ -1879,7 +1889,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu if {$i > 243} { set fg "web-black" } - append out "[a+ {*}$fc {*}$fg Term$i][format %3s $i] " + append out "[a+ {*}$fc {*}$fg Term-$i][format %3s $i] " } return $out[a] @@ -1899,7 +1909,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [textblock::class::table new] $t configure -show_hseps 0 -show_edge 0 for {set i 232} {$i <=255} {incr i} { - set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term$i - may as well let a+ cache the call by name as the preferred? option + set cname [tcl::dict::get $TERM_colour_map_reverse $i] ;#use term-cname etc instead of term-$i - may as well let a+ cache the call by name as the preferred? option if {[llength $row]== 8} { lappend rows $row set row [list] @@ -1919,6 +1929,169 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return [tcl::string::trimleft $out \n] } + + if {[catch {package require punk::ansi::colourmap} errM]} { + puts stderr "punk::ansi FAILED to load punk::ansi::colourmap\n$errM" + } + if {[info exists ::punk::ansi::colourmap::TK_colour_map]} { + upvar ::punk::ansi::colourmap::TK_colour_map TK_colour_map + upvar ::punk::ansi::colourmap::TK_colour_map_lookup TK_colour_map_lookup + } else { + puts stderr "Failed to find TK_colour_map - punk::ansi::colourmap package not loaded?" + variable TK_colour_map {} + variable TK_colour_map_lookup {} + } + + #order-preserving + proc lunique {list} { + set new {} + foreach item $list { + if {$item ni $new} { + lappend new $item + } + } + return $new + } + + proc colourtable_tk {args} { + set opts {-forcecolour 0 -groups * -merged 0 -globs *} + foreach {k v} $args { + switch -- $k { + -groups - -merged - -forcecolour - -globs { + tcl::dict::set opts $k $v + } + default { + error "colourtable_tk unrecognised option '$k'. Known-options: [tcl::dict::keys $opts]" + } + } + } + set fc "" + if {[tcl::dict::get $opts -forcecolour]} { + set fc "forcecolour" + } + + #not implemented - todo? Tk + set groups [tcl::dict::get $opts -groups] + + set do_merge [tcl::dict::get $opts -merged] + set globs [tcl::dict::get $opts -globs] + + + + set blocklist [list] ;#vertical blocks consisting of blockrows + set blockrow [list] + set height 50 ;#number of lines (excluding header) vertically in a blockrow + set columns 5 ;#number of columns in a blockrow + variable TK_colour_map ;#use the version without lowercased additions - this gives the display names with casing as shown in Tk colour man page. + if {!$do_merge} { + set map $TK_colour_map + if {$globs eq "*"} { + set keys [dict keys $TK_colour_map] + } else { + set keys [list] + set mapkeys [dict keys $TK_colour_map] + foreach g $globs { + #lappend keys {*}[dict keys $map $g] + #need case insensitive globs for convenience. + lappend keys {*}[lsearch -all -glob -inline -nocase $mapkeys $g] + } + set keys [lunique $keys] + } + } else { + #todo - make glob fully search when do_merge + #needs to get keys from all names - but then map to keys that have dependent names + upvar ::punk::ansi::colourmap::TK_colour_map_merge map + upvar ::punk::ansi::colourmap::TK_colour_map_reverse reversemap + if {$globs eq "*"} { + set keys [dict keys $map] + } else { + set keys [list] + set allkeys [dict keys $TK_colour_map] + + foreach g $globs { + set matchedkeys [lsearch -all -glob -inline -nocase $allkeys $g] + foreach m $matchedkeys { + if {![dict exists $map $m]} { + #not a parent in a merge + set rgb [dict get $TK_colour_map $m] + set names [dict get $reversemap $rgb] + #first name is the one that is in the merge map + lappend keys [lindex $names 0] + } else { + lappend keys $m + } + } + } + set keys [lunique $keys] + } + } + set overheight 0 + + + set t "" + set start 0 + set colidx -1 + set i -1 + foreach cname $keys { + incr i + set data [dict get $map $cname] + if {$overheight || $i % $height == 0} { + set overheight 0 + incr colidx + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + $t destroy + if {$colidx % $columns == 0} { + lappend blocklist $blockrow + set blockrow [list] + } + } + set start $i + set t [textblock::class::table new] + $t configure -show_edge 0 -show_seps 0 -show_header 1 -minwidth 42 + } + if {!$do_merge} { + set cdec $data + $t add_row [list $cname " [colour_dec2hex $cdec] " $cdec] + } else { + set cdec [dict get $data colour] + set othernames [dict get $data names] + set ndisplay [join [list $cname {*}$othernames] \n] + $t add_row [list $ndisplay " [colour_dec2hex $cdec] " $cdec] + set overheight 0 + foreach n $othernames { + incr i + if {$i % $height == 0} { + set overheight 1 + } + } + } + set fg "rgb-$cdec-contrasting" + $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] + } + + if {$t ne ""} { + $t configure -frametype {} + $t configure_column 0 -headers [list "TK colours $start - $i"] + $t configure_column 0 -header_colspans [list any] + $t configure -ansibase_header [a+ {*}$fc web-black Web-white] + lappend blockrow [$t print] " " + lappend blocklist $blockrow + $t destroy + } + + set result "" + foreach blockrow $blocklist { + append result [textblock::join -- {*}$blockrow] \n + } + + return $result + } + #set WEB_colour_map [tcl::dict::merge\ # $WEB_colour_map_basic\ # $WEB_colour_map_pink\ @@ -1970,17 +2143,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } set grouptables [list] - set white_fg_list [list\ - mediumvioletred deeppink\ - darkred red firebrick crimson indianred\ - orangered\ - maroon brown saddlebrown sienna\ - indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ - midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ - teal darkcyan\ - darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ - black darkslategray dimgray slategray\ - ] + #set white_fg_list [list\ + # mediumvioletred deeppink\ + # darkred red firebrick crimson indianred\ + # orangered\ + # maroon brown saddlebrown sienna\ + # indigo purple darkmagenta darkviolet darkslateblue blueviolet darkorchid fuchsia magenta slateblue mediumslateblue\ + # midnightblue navy darkblue mediumblue blue royalblue steelblue dodgerblue\ + # teal darkcyan\ + # darkgreen green darkolivegreen forestgreen seagreen olive olivedrab\ + # black darkslategray dimgray slategray\ + # ] foreach g $show_groups { #upvar WEB_colour_map_$g map_$g variable WEB_colour_map_$g @@ -1988,11 +2161,12 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t configure -show_edge 0 -show_seps 0 -show_header 1 tcl::dict::for {cname cdec} [set WEB_colour_map_$g] { $t add_row [list "$cname " "[colour_dec2hex $cdec] " $cdec] - if {$cname in $white_fg_list} { - set fg "web-white" - } else { - set fg "web-black" - } + set fg "rgb-$cdec-contrasting" + #if {$cname in $white_fg_list} { + # set fg "web-white" + #} else { + # set fg "web-black" + #} #$t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Rgb-$cdec] $t configure_row [expr {[$t row_count]-1}] -ansibase [a+ {*}$fc $fg Web-$cname] } @@ -2083,12 +2257,66 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu return $displaytable } + lappend PUNKARGS [list { + @id -id ::punk::ansi::a? + @cmd -name "punk::ansi::a?"\ + -summary\ + "ANSI colour information"\ + -help\ + "" + @form -form "sgr_overview" + @values -form "sgr_overview" -min 0 -max 0 + + + @form -form "term" + @leaders -form "term" -min 1 -max 1 + term -type literal(term) -help\ + "256 term colours" + @opts -min 0 -max 0 + @values -form "term" -min 0 -max -1 + panel -type string -optional 1 -multiple 1 -default {16 main greyscale note}\ + -choices {16 main greyscale pastel rainbow note} + + @form -form "tk" + @leaders -form "tk" -min 1 -max 1 + tk -type literal(tk)|literal(TK) -help\ + "Tk colours" + @opts -form "tk" + -merged -type none -help\ + "If this flag is supplied - show colour names with the same RGB + values together." + @values -form "tk" -min 0 -max -1 + glob -type string -optional 1 -multiple 1 -help\ + "A glob string such as *green*. + Multiple glob entries can be provided. The search is case insensitive" + + + @form -form "web" + @values -form "web" -min 1 -max -1 + web -type literal(web) -help\ + "Web colours" + panel -type string -optional 1 -multiple 1 -choices {basic pink red orange yellow brown purple blue cyan green white gray} + + @form -form "x11" + @values -form "x11" -min 1 -max 1 + x11 -type literal(x11) -help\ + "x11 colours" + + + @form -form "sample" + @values -form "sample" -min 1 -max -1 + colourcode -type sgr|Sgr|literalprefix(term-)|literalprefix(Term-)|literalprefix(web-)|literalprefix(Web-)|literalprefix(rgb)|literalprefix(Rgb)\ + -optional 0\ + -multiple 1 + + }] proc a? {args} { #*** !doctools #[call [fun a?] [opt {ansicode...}]] #[para]Return an ansi string representing a table of codes and a panel showing the colours variable SGR_setting_map variable SGR_colour_map + variable TK_colour_map_lookup set fcposn [lsearch $args "force*"] set fc "" set opt_forcecolour 0 @@ -2172,6 +2400,20 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu append out \n append out "[a+ {*}$fc web-white]X11 colours[a] - mostly match Web colours" \n append out [textblock::join -- $indent "To see differences: a? x11"] \n + append out \n + append out "[a+ {*}$fc web-white]Tk colours[a]" \n + append out [textblock::join -- $indent "To see all 750+ names use: a? tk"] \n + append out [textblock::join -- $indent "Restrict the results using globs e.g a? tk *green* *yellow*"] \n + append out [textblock::join -- $indent "The foreground colour in this table is generated using the contrasting suffix"] \n + append out [textblock::join -- $indent "Example: \[a+ tk-tan-contrasting Tk-tan\]text\[a] -> [a+ {*}$fc tk-tan-contrasting Tk-tan]text[a]"] \n + append out \n + append out "[a+ {*}$fc web-white]Combination testing[a]" \n + append out [textblock::join -- $indent "Example: a? red brightgreen underline Tk-slategrey italic"] \n + append out [textblock::join -- $indent "This will show a small table of each applied code and a RESULT row. The 'red' in this case is redundant,"] \n + append out [textblock::join -- $indent "so a final MERGED row displays with an alert 'REDUNDANCIES FOUND'."] \n + append out [textblock::join -- $indent "The final columns of RESULT and MERGED (showing raw ANSI sequence) will differ if the arguments aren't in canonical order."] \n + append out [textblock::join -- $indent "The MERGED line will only display if there are redundancies or different ordering."] \n + if {[tcl::info::exists ::punk::console::colour_disabled] && $::punk::console::colour_disabled} { append out \n @@ -2191,40 +2433,74 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { switch -- [lindex $args 0] { term { - set termargs [lrange $args 1 end] - foreach ta $termargs { - switch -- $ta { - pastel - rainbow {} - default {error "unrecognised term option '$ta'. Known values: pastel rainbow"} - } - } - set out "16 basic colours\n" - append out [colourtable_16_names -forcecolour $opt_forcecolour] \n - append out "216 colours\n" - append out [colourtable_216_names -forcecolour $opt_forcecolour] \n - append out "24 greyscale colours\n" - append out [colourtable_24_names -forcecolour $opt_forcecolour] - foreach ta $termargs { - switch -- $ta { + set argd [punk::args::parse $args -form "term" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + set panels [dict get $values panel] + + set out "" + foreach panel $panels { + #punk::args has already resolved prefixes to full panel names + switch -- $panel { + 16 { + append out "16 basic colours\n" + append out [colourtable_16_names -forcecolour $opt_forcecolour] \n + } + main { + append out "216 colours\n" + append out [colourtable_216_names -forcecolour $opt_forcecolour] \n + } + note { + append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" \n + append out " grey vs gray (UK/US spelling) - these are inconsistent for historical reasons. e.g grey0,lightslategrey,darkslategray1" \n + } + greyscale { + append out "24 greyscale colours\n" + append out [colourtable_24_names -forcecolour $opt_forcecolour] \n + } pastel { - append out \n append out "Pastel Colour Space (punk::ansi::colourtable_term_pastel)\n" - append out [colourtable_term_pastel -forcecolour $opt_forcecolour] + append out [colourtable_term_pastel -forcecolour $opt_forcecolour] \n } rainbow { - append out \n append out "Rainbow Colours (punk::ansi::colourtable_term_rainbow)\n" - append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] + append out [colourtable_term_rainbow -forcecolour $opt_forcecolour] \n + } + default { + #only reachable if punk::args definition is out of sync + set panelnames {16 main greyscale pastel rainbow note} + append out "(ERROR: unrecognised panel '$ta' for 'a? term'. Known values $panelnames)" } } } - append out "\nNote: The 256 term colours especially 0-15 may be altered by terminal palette settings or ansi OSC 4 codes, so specific RGB values are unavailable" return $out } web { - return [colourtable_web -forcecolour $opt_forcecolour -groups [lrange $args 1 end]] + set argd [punk::args::parse $args -form "web" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received panel]} { + set panels [dict get $values panel] + } else { + set panels {*} + } + return [colourtable_web -forcecolour $opt_forcecolour -groups $panels] + } + tk - TK { + set argd [punk::args::parse $args -form "tk" -errorstyle standard withid ::punk::ansi::a?] + lassign [dict values $argd] leaders opts values received + if {[dict exists $received glob]} { + set globs [dict get $values glob] + } else { + set globs {*} + } + if {[dict exists $received -merged]} { + set ismerged 1 + } else { + set ismerged 0 + } + return [colourtable_tk -merged $ismerged -forcecolour $opt_forcecolour -globs $globs] } x11 { + set argd [punk::args::parse $args -form "x11" -errorstyle standard withid ::punk::ansi::a?] set out "" append out " Mostly same as web - known differences displayed" \n append out [colourtable_x11diff -forcecolour $opt_forcecolour] @@ -2243,10 +2519,11 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set resultlist [list] foreach i $args { - set f4 [tcl::string::range $i 0 3] + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [::split $i "-# "] 0] set s [a+ {*}$fc $i]sample - switch -- $f4 { - web- - Web- - WEB- { + switch -- $pfx { + web - Web - WEB { set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] set cont [string range $tail end-11 end] switch -- $cont { @@ -2275,7 +2552,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i $descr $s [ansistring VIEW $s]] } term - Term - undt { - set tail [tcl::string::trim [tcl::string::range $i 4 end] -] + set tail [tcl::string::range $i 5 end] if {[tcl::string::is integer -strict $tail]} { if {$tail < 256} { set descr "[tcl::dict::get $TERM_colour_map_reverse $tail]" @@ -2292,10 +2569,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - x11- - X11- { - set tail [tcl::string::tolower [tcl::string::trim [tcl::string::range $i 4 end] -]] - if {[tcl::dict::exists $X11_colour_map $tail]} { - set dec [tcl::dict::get $X11_colour_map $tail] + x11 - X11 { + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] + set cont [string range $cname end-11 end] + switch -exact -- $cont {-contrasting - -contrastive {set cname [string range $tail end-12]}} + + if {[tcl::dict::exists $X11_colour_map $cname]} { + set dec [tcl::dict::get $X11_colour_map $cname] set hex [colour_dec2hex $dec] set descr "$hex $dec" } else { @@ -2303,12 +2583,27 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } $t add_row [list $i $descr $s [ansistring VIEW $s]] } - rgb- - Rgb- - RGB- - - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 - - rgb# - Rgb# - RGB# - - und# - und- { + tk - Tk { + set tail [tcl::string::tolower [tcl::string::range $i 3 end]] + set cont [string range $tail end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set cname [string range $tail 0 end-12] + } + default { + set cname $tail + } + } + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set dec [tcl::dict::get $TK_colour_map_lookup $cname] + set hex [colour_dec2hex $dec] + set descr "$hex $dec" + } else { + set descr "UNKNOWN colour for tk" + } + $t add_row [list $i $descr $s [ansistring VIEW $s]] + } + rgb - Rgb - RGB - und { set cont [string range $i end-11 end] switch -- $cont { -contrasting - -contrastive { @@ -2339,7 +2634,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } set info "$hexfinal $decfinal" ;#show opposite type as first line of info col } else { - set tail [tcl::string::trim [tcl::string::range $iplain 3 end] -] + set tail [tcl::string::range $iplain 4 end] set dec $tail switch -- $cont { -contrasting { @@ -2369,15 +2664,23 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend x11colours $c } } + if {[dict exists $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal]} { + set tkcolours [dict get $::punk::ansi::colourmap::TK_colour_map_reverse $decfinal] + } else { + set tkcolours [list] + } foreach c $webcolours { append info \n web-$c } foreach c $x11colours { append info \n x11-$c } + foreach c $tkcolours { + append info \n tk-$c + } $t add_row [list $i "$info" $s [ansistring VIEW $s]] } - unde { + default { switch -- $i { undercurly - undercurl - underdotted - underdot - underdashed - underdash - undersingle - underdouble { $t add_row [list $i extended $s [ansistring VIEW $s]] @@ -2389,19 +2692,17 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu $t add_row [list $i "SGR 59" $s [ansistring VIEW $s]] } default { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] - } - } - } - default { - if {[tcl::string::is integer -strict $i]} { - set rmap [lreverse $SGR_map] - $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] - } else { - if {[tcl::dict::exists $SGR_map $i]} { - $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] - } else { - $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + #$t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + if {[tcl::string::is integer -strict $i]} { + set rmap [lreverse $SGR_map] + $t add_row [list $i "SGR [tcl::dict::get $rmap $i]" $s [ansistring VIEW $s]] + } else { + if {[tcl::dict::exists $SGR_map $i]} { + $t add_row [list $i "SGR [tcl::dict::get $SGR_map $i]" $s [ansistring VIEW $s]] + } else { + $t add_row [list $i UNKNOWN $s [ansistring VIEW $s]] + } + } } } } @@ -2541,24 +2842,19 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes needing to go in own escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + set pfx [lindex [::split $i "-# "] 0] + #set f4 [tcl::string::range $i 0 3] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour - set tail [tcl::string::tolower [tcl::string::range $i 4 end]] + set cname [tcl::string::tolower [tcl::string::range $i 4 end]] #-contrasting #-contrastive - set cont [string range $tail end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set cname [string range $tail 0 end-12] - } - default { - set cname $tail - } - } + set cont [string range $cname end-11 end] + switch -- $cont { -contrasting - -contrastive {set cname [string range $cname 0 end-12]} } + if {[tcl::dict::exists $WEB_colour_map $cname]} { set rgbdash [tcl::dict::get $WEB_colour_map $cname] switch -- $cont { @@ -2577,7 +2873,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a+ $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -2609,140 +2905,94 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a+ $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6 } + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - #TODO - fix - # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. - # need to emit in - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #NOTE struct::set result order can differ depending on whether tcl/critcl imp used - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline {lappend t 4} + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #NOTE struct::set result order can differ depending on whether tcl/critcl imp used + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + #TODO - fix + # extended codes with colon suppress normal SGR attributes when in same escape sequence on terminal that don't support the extended codes. + # need to emit in + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund {lappend t 59} + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer @@ -2772,105 +3022,112 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 - - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb foreground/background - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting - -contrastive { - set iplain [string range $i 0 end-12] + rgb - Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground/background + #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting - -contrastive { + set iplain [string range $i 0 end-12] + } + default { + set iplain $i + } } - default { - set iplain $i + set rgbspec [tcl::string::range $iplain 4 end] + set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - } - set rgbspec [tcl::string::trim [tcl::string::range $iplain 3 end] -] - set RGB [tcl::string::map [list - { } , { } {;} { }] $rgbspec] ;#RGB as list - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + if {[tcl::string::index $i 0] eq "r"} { + #fg + lappend t "38;2;$rgbfinal" + } else { + #bg + lappend t "48;2;$rgbfinal" } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + + } elseif {$utype eq "#"} { + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + } + default { + set rgbfinal [join $RGB {;}] + } } - default { - set rgbfinal [join $RGB {;}] + if {[tcl::string::index $i 0] eq "r"} { + #hex rgb foreground + lappend t "38;2;$rgbfinal" + } else { + #hex rgb background + lappend t "48;2;$rgbfinal" } - } - if {[tcl::string::index $i 0] eq "r"} { - #fg - lappend t "38;2;$rgbfinal" } else { - #bg - lappend t "48;2;$rgbfinal" + puts stderr "punk::ansi::a+ ansi term rgb colour unmatched: '$i' in call 'a+ $args'" } } - "rgb#" - "Rgb#" - "RGB#" { - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {;}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {;}] + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx + #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb + set rgbspec [tcl::string::range $i 4 end] + set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] + #puts "---->'$RGB'<----" + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - default { - set rgbfinal [join $RGB {;}] + #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? + lappend e "58:2::$rgbfinal" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + set RGB [::scan $hex6 %2X%2X%2X] + set cont [string range $i end-11 end] + switch -- $cont { + -contrasting { + set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] + } + -contrastive { + set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] + } + default { + set rgbfinal [join $RGB {:}] + } } - } - if {[tcl::string::index $i 0] eq "r"} { - #hex rgb foreground - lappend t "38;2;$rgbfinal" + lappend e "58:2::$rgbfinal" } else { - #hex rgb background - lappend t "48;2;$rgbfinal" - } - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - #https://wezfurlong.org/wezterm/escape-sequences.html#csi-582-underline-color-rgb - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set RGB [lrange [tcl::string::map [list - { } , { } {;} { }] $rgbspec] 0 2] - #puts "---->'$RGB'<----" - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } - } - #lappend e "58:2:$rgbfinal" ;# - no colorspace ID - some terminals support? which? - lappend e "58:2::$rgbfinal" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - #set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - set RGB [::scan $hex6 %2X%2X%2X] - set cont [string range $i end-11 end] - switch -- $cont { - -contrasting { - set rgbfinal [join [punk::ansi::colour::contrasting {*}$RGB] {:}] - } - -contrastive { - set rgbfinal [join [lindex [punk::ansi::colour::contrast_pair {*}$RGB] 0] {:}] - } - default { - set rgbfinal [join $RGB {:}] - } + puts stderr "punk::ansi::a+ ansi term underline colour unmatched: '$i' in call 'a+ $args'" } - lappend e "58:2::$rgbfinal" } undt { #CSI 58:5 UNDERLINE COLOR PALETTE INDEX @@ -2878,7 +3135,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -2889,7 +3146,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2898,10 +3155,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i' in call 'a+ $args'" + puts stderr "ansi x11 foreground colour unmatched: '$i' in call 'a+ $args'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -2910,7 +3167,59 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + #foreground tk names + variable TK_colour_map_lookup ;#use the dict with added lowercase versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -exact -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i' in call 'a+ $args'" + } + } + Tk - TK { + #background X11 names + variable TK_colour_map_lookup ;#with lc versions + + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + lassign [punk::lib::string_splitbefore $cname end-11] c cont + switch -- $cont { -contrasting - -contrastive {set cname $c} } + + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + switch -- $cont { + -contrasting { + set rgb [join [punk::ansi::colour::contrasting {*}[split $rgbdash -]] {;}] + } + -contrastive { + set rgb [join [lindex [punk::ansi::colour::contrast_pair {*}[split $rgbdash -]] 0] {;}] + } + default { + set rgb [tcl::string::map { - ;} $rgbdash] + } + } + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -2919,7 +3228,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a+ ansi name unmatched: '$i' in call 'a+ $args' Perhaps missing prefix? e.g web- x11- tk- term- rgb# rgb-" } } } @@ -2974,6 +3283,32 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #indent of 1 space is important for clarity in i -return string a+ output dict set SGR_samples $k " [punk::ansi::a+ $k]sample\x1b\[m" } + set SGR_help\ + {SGR code from the list below, or an integer corresponding to the code e.g 31 = red. + A leading capital letter indicates a codename applies to the background colour. + Other accepted codes are: + term- Term- foreground/background where int is 0-255 terminal color + term- Term- foreground/background + + web- Web- + + x11- X11- + + tk- Tk- + + rgb--- Rgb--- foreground/background where are the + 0-255 int values for red, green and blue. + rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 + + The acceptable values for colours can be queried using + punk::ansi::a? term + punk::ansi::a? web + punk::ansi::a? x11 + punk::ansi::a? tk + + Example to set foreground red and background cyan followed by a reset: + set str \"[a+ red Cyan]sample text[a]\" + } lappend PUNKARGS [list { @id -id ::punk::ansi::a+ @cmd -name "punk::ansi::a+" -help\ @@ -2981,28 +3316,25 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Unlike punk::ansi::a - it is not prefixed with an ANSI reset. " @values -min 0 -max -1 - } [string map [list [dict keys $SGR_map] $SGR_samples] { - code -type string -optional 1 -multiple 1 -choices {}\ - -choicelabels {}\ + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ - "SGR code from the list below, or an integer corresponding to the code e.g 31 = red. - A leading capital letter indicates a codename applies to the background colour. - Other accepted codes are: - term- Term- foreground/background where int is 0-255 terminal color - term- Term- foreground/background - rgb--- Rgb--- foreground/background where are the - 0-255 int values for red, green and blue. - rgb# Rgb# where is a 6 char hex colour e.g rgb#C71585 - web- Web- - - The acceptable values for and can be queried using - punk::ansi::a? term - and - punk::ansi::a? web - - Example to set foreground red and background cyan followed by a reset: - set str \"[a+ red Cyan]sample text[a]\" + "%SGR_help%" + }]] + + lappend PUNKARGS [list { + @id -id ::punk::ansi::a + @cmd -name "punk::ansi::a" -help\ + "Returns an ANSI sgr escape sequence based on the list of supplied codes. + Unlike punk::ansi::a+ - it is prefixed with an ANSI reset. " + @values -min 0 -max -1 + } [string map [list %choices% [dict keys $SGR_map] %choicelabels% $SGR_samples %SGR_help% $SGR_help] { + code -type string -optional 1 -multiple 1 -choices {%choices%}\ + -choicelabels {%choicelabels%}\ + -choicecolumns 5 -choiceprefix 0 -choicerestricted 0 -help\ + "%SGR_help%" }]] proc a {args} { @@ -3027,6 +3359,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #we want this to be available to call even if ansi is off variable WEB_colour_map variable TERM_colour_map + variable TK_colour_map_lookup ;#Tk accepts lowercase versions of colours even though some colours are documented with casing set colour_disabled 0 #whatever function disables or re-enables colour should have made a call to punk::ansi::sgr_cache -action clear @@ -3044,9 +3377,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set t [list] set e [list] ;#extended codes will suppress standard SGR colours and attributes if merged in same escape sequence foreach i $args { - set f4 [tcl::string::range $i 0 3] - switch -- $f4 { - web- { + #set f4 [tcl::string::range $i 0 3] + set pfx [lindex [split $i "-# "] 0] + switch -- $pfx { + web { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #foreground web colour @@ -3059,7 +3393,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi web colour unmatched: '$i' in call 'a $args'" } } - Web- - WEB- { + Web - WEB { #variable WEB_colour_map #upvar ::punk::ansi::WEB_colour_map WEB_colour_map #background web colour @@ -3070,142 +3404,100 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu puts stderr "ansi Web colour unmatched: '$i' in call 'a $args'" } } - rese {lappend t 0 ;#reset} + reset {lappend t 0} bold {lappend t 1} dim {lappend t 2} - blin { - #blink - lappend t 5 - } - fast { - #fastblink - lappend t 6 - } - nobl { - #noblink - lappend t 25 - } + blink {lappend t 5} + fastblink {lappend t 6} + noblink {lappend t 25} hide {lappend t 8} - norm {lappend t 22 ;#normal} - unde { - switch -- $i { - underline { - lappend t 4 ;#underline - } - underlinedefault { - lappend t 59 - } - underextendedoff { - #lremove any existing 4:1 etc - #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) - #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] - set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] - lappend e 4:0 - } - undersingle { - lappend e 4:1 - } - underdouble { - lappend e 4:2 - } - undercurly - undercurl { - lappend e 4:3 - } - underdotted - underdot { - lappend e 4:4 - } - underdashed - underdash { - lappend e 4:5 - } - default { - puts stderr "ansi term unmatched: unde* '$i' in call 'a $args' (underline,undersingle,underdouble,undercurly,underdotted,underdashed)" - } - } - } - doub {lappend t 21 ;#doubleunderline} - noun { + normal {lappend t 22} + underline { + lappend t 4 ;#underline + } + underlinedefault {lappend t 59} + underextendedoff { + #lremove any existing 4:1 etc + #use of struct::set with critcl is fast, but will reorder e (with differences depending on tcl vs critcl) + #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] + set e [punk::lib::ldiff $e [list 4:1 4:2 4:3 4:4 4:5]] + lappend e 4:0 + } + undersingle { + lappend e 4:1 + } + underdouble { + lappend e 4:2 + } + undercurly - undercurl { + lappend e 4:3 + } + underdotted - underdot { + lappend e 4:4 + } + underdashed - underdash { + lappend e 4:5 + } + doubleunderline {lappend t 21} + nounderline { lappend t 24 ;#nounderline #set e [struct::set difference $e [list 4:1 4:2 4:3 4:4 4:5]] lappend e 4:0 } - stri {lappend t 9 ;#strike} - nost {lappend t 29 ;#nostrike} - ital {lappend t 3 ;#italic} - noit {lappend t 23 ;#noitalic} - reve {lappend t 7 ;#reverse} - nore {lappend t 27 ;#noreverse} - defa { - switch -- $i { - defaultfg { - lappend t 39 - } - defaultbg { - lappend t 49 - } - defaultund { - lappend t 59 - } - default { - puts stderr "ansi term unmatched: defa* '$i' in call 'a $args' (defaultfg,defaultbg,defaultund)" - } - } - } - nohi {lappend t 28 ;#nohide} - over {lappend t 53 ;#overline} - noov {lappend t 55 ;#nooverline} - fram { - if {$i eq "frame"} { - lappend t 51 ;#frame - } else { - lappend t 52 ;#framecircle - } - } - nofr {lappend t 54 ;#noframe} - blac {lappend t 30 ;#black} + strike {lappend t 9} + nostrike {lappend t 29} + italic {lappend t 3} + noitalic {lappend t 23} + reverse {lappend t 7} + noreverse {lappend t 27} + defaultfg {lappend t 39} + defaultbg {lappend t 49} + defaultund { + lappend t 59 + } + nohide {lappend t 28} + overline {lappend t 53} + nooverline {lappend t 55} + frame {lappend t 51} + framecircle {lappend t 52} + noframe {lappend t 54} + black {lappend t 30} red {lappend t 31} - gree {lappend t 32 ;#green} - yell {lappend t 33 ;#yellow} + green {lappend t 32} + yellow {lappend t 33} blue {lappend t 34} - purp {lappend t 35 ;#purple} + purple {lappend t 35} cyan {lappend t 36} - whit {lappend t 37 ;#white} - Blac {lappend t 40 ;#Black} + white {lappend t 37} + Black {lappend t 40} Red {lappend t 41} - Gree {lappend t 42 ;#Green} - Yell {lappend t 43 ;#Yellow} + Green {lappend t 42} + Yellow {lappend t 43} Blue {lappend t 44} - Purp {lappend t 45 ;#Purple} + Purple {lappend t 45} Cyan {lappend t 46} - Whit {lappend t 47 ;#White} - brig { - switch -- $i { - brightblack {lappend t 90} - brightred {lappend t 91} - brightgreen {lappend t 92} - brightyellow {lappend t 93} - brightblue {lappend t 94} - brightpurple {lappend t 95} - brightcyan {lappend t 96} - brightwhite {lappend t 97} - } - } - Brig { - switch -- $i { - Brightblack {lappend t 100} - Brightred {lappend t 101} - Brightgreen {lappend t 102} - Brightyellow {lappend t 103} - Brightblue {lappend t 104} - Brightpurple {lappend t 105} - Brightcyan {lappend t 106} - Brightwhite {lappend t 107} - } - } + White {lappend t 47} + brightblack {lappend t 90} + brightred {lappend t 91} + brightgreen {lappend t 92} + brightyellow {lappend t 93} + brightblue {lappend t 94} + brightpurple {lappend t 95} + brightcyan {lappend t 96} + brightwhite {lappend t 97} + Brightblack {lappend t 100} + Brightred {lappend t 101} + Brightgreen {lappend t 102} + Brightyellow {lappend t 103} + Brightblue {lappend t 104} + Brightpurple {lappend t 105} + Brightcyan {lappend t 106} + Brightwhite {lappend t 107} term { #variable TERM_colour_map #256 colour foreground by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend t "38;5;$cc" } else { @@ -3219,7 +3511,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu Term - TERM { #variable TERM_colour_map #256 colour background by Xterm name or by integer - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] && $cc < 256} { lappend t "48;5;$cc" } else { @@ -3230,49 +3522,63 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - rgb- - rgb0 - rgb1 - rgb2 - rgb3 - rgb4 - rgb5 - rgb6 - rgb7 - rgb8 - rgb9 { - #decimal rgb foreground - #allow variants rgb-xxx-xxx-xxx and rgbxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "38;2;$rgb" - } - Rgb- - RGB- - Rgb0 - Rgb1 - Rgb2 - Rgb3 - Rgb4 - Rgb5 - Rgb6 - Rgb7 - Rgb8 - Rgb9 - RGB0 - RGB1 - RGB2 - RGB3 - RGB4 - RGB5 - RGB6 - RGB7 - RGB8 - RGB9 { - #decimal rgb background - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] - lappend t "48;2;$rgb" - } - "rgb#" { - #hex rgb foreground - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "38;2;$rgb" - } - "Rgb#" - "RGB#" { - #hex rgb background - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {;}] - lappend t "48;2;$rgb" - } - und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 { - #decimal rgb underline - #allow variants und-xxx-xxx-xxx and undxxx-xxx-xxx - set rgbspec [tcl::string::trim [tcl::string::range $i 3 end] -] - set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] - lappend e "58:2::$rgb" - } - "und#" { - #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators - set hex6 [tcl::string::trim [tcl::string::range $i 4 end] -] - set rgb [join [::scan $hex6 %2X%2X%2X] {:}] - lappend e "58:2::$rgb" + rgb { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb foreground + #form: rgb-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "38;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb foreground + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi rgb foreground colour unmatched: '$i' in call 'a $args'" + } + } + Rgb - RGB { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb background + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {;} , {;}] $rgbspec] + lappend t "48;2;$rgb" + } elseif {$utype eq "#"} { + #hex rgb background + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {;}] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Rgb background colour unmatched: '$i' in call 'a $args'" + } + } + und- - und0 - und1 - und2 - und3 - und4 - und5 - und6 - und7 - und8 - und9 {} + und { + set utype [string index $i 3] + if {$utype eq "-"} { + #decimal rgb underline + #form: und-xxx-xxx-xxx + set rgbspec [tcl::string::range $i 4 end] + set rgb [tcl::string::map [list - {:} , {:}] $rgbspec] + lappend e "58:2::$rgb" + } elseif {$utype eq "#"} { + #hex rgb underline - (e.g kitty, wezterm) - uses colons as separators + set hex6 [tcl::string::range $i 4 end] + set rgb [join [::scan $hex6 %2X%2X%2X] {:}] + lappend e "58:2::$rgb" + } else { + puts stderr "ansi underline colour unmatched: '$i' in call 'a $args'" + } } undt { #variable TERM_colour_map #256 colour underline by Xterm name or by integer #name is xterm name or colour index from 0 - 255 - set cc [tcl::string::trim [tcl::string::tolower [tcl::string::range $i 4 end]] -] + #undt-<0-255> or undt- + set cc [tcl::string::tolower [tcl::string::range $i 5 end]] if {[tcl::string::is integer -strict $cc] & $cc < 256} { lappend e "58:5:$cc" } else { @@ -3283,7 +3589,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } } } - x11- { + x11 { variable X11_colour_map #foreground X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3292,10 +3598,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "38;2;$rgb" } else { - puts stderr "ansi x11 colour unmatched: '$i'" + puts stderr "ansi x11 foreground colour unmatched: '$i'" } } - X11- { + X11 { variable X11_colour_map #background X11 names set cname [tcl::string::tolower [tcl::string::range $i 4 end]] @@ -3304,7 +3610,31 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rgb [tcl::string::map [list - {;}] $rgbdash] lappend t "48;2;$rgb" } else { - puts stderr "ansi X11 colour unmatched: '$i'" + puts stderr "ansi X11 background colour unmatched: '$i'" + } + } + tk { + variable TK_colour_map_lookup + #foreground tk names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "38;2;$rgb" + } else { + puts stderr "ansi tk foreground colour unmatched: '$i'" + } + } + Tk - TK { + variable TK_colour_map_lookup + #background X11 names + set cname [tcl::string::tolower [tcl::string::range $i 3 end]] + if {[tcl::dict::exists $TK_colour_map_lookup $cname]} { + set rgbdash [tcl::dict::get $TK_colour_map_lookup $cname] + set rgb [tcl::string::map [list - {;}] $rgbdash] + lappend t "48;2;$rgb" + } else { + puts stderr "ansi Tk background colour unmatched: '$i'" } } default { @@ -3313,7 +3643,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } elseif {[tcl::string::first : $i] > 0} { lappend e $i } else { - puts stderr "ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" + puts stderr "punk::ansi::a ansi name unmatched: '$i' in call 'a $args' Perhaps missing prefix? e.g web- x11- term- rgb# rgb-" } } } @@ -3356,7 +3686,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu lappend PUNKARGS [list { @id -id ::punk::ansi::ansiwrap - @cmd -name punk::ansi::ansiwrap -help\ + @cmd -name punk::ansi::ansiwrap\ + -summary\ + "Wrap a string with ANSI codes applied when not overridden by ANSI in the source string."\ + -help\ {Wrap a string with ANSI codes from supplied codelist(s) followed by trailing ANSI reset. The wrapping is done such that @@ -3395,12 +3728,10 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu -rawansi -type ansi -default "" -resetcodes -type list -default {reset} -rawresets -type ansi -default "" - -fullcodemerge -type boolean -default 0 -help\ - "experimental" -overridecodes -type list -default {} -rawoverrides -type ansi -default "" @values -min 1 -max 1 - text -type string -help\ + text -type any -help\ "String to wrap with ANSI (SGR)" }] proc ansiwrap {args} { @@ -3411,13 +3742,13 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } #we know there are no valid codes that start with - if {[lsearch [lrange $args 0 end-1] -*] == -1} { - #no opts - set text [lindex $args end] - set codelists [lrange $args 0 end-1] - set R [a] ;#plain ansi reset + #no opts - skip args parser + #maint: keep defaults in sync with definition above + set codelists $args + set text [lpop codelists] + set R [a] ;#plain ansi reset (equiv of default "reset") set rawansi "" set rawresets "" - set fullmerge 0 set overrides "" set rawoverrides "" } else { @@ -3428,7 +3759,6 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set rawansi [dict get $opts -rawansi] set R [a+ {*}[dict get $opts -resetcodes]] set rawresets [dict get $opts -rawresets] - set fullmerge [dict get $opts -fullcodemerge] set overrides [punk::ansi::ta::get_codes_single [a+ {*}[dict get $opts -overridecodes]]] set rawoverrides [punk::ansi::ta::get_codes_single [dict get $opts -rawoverrides]] } @@ -3437,22 +3767,18 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes set codes [concat {*}$codelists] ;#flatten set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] if {$rawansi ne ""} { set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] - if {$fullmerge} { - set base [punk::ansi::codetype::sgr_merge [list $base {*}$rawcodes]] - } else { - set base [punk::ansi::codetype::sgr_merge_singles [list $base {*}$rawcodes]] - } + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] } if {$rawresets ne ""} { set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] - if {$fullmerge} { - set R [punk::ansi::codetype::sgr_merge [list $R {*}$rawresetcodes]] - } else { - set R [punk::ansi::codetype::sgr_merge_singles [list $R {*}$rawresetcodes]] - } + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] } + if {$rawoverrides ne ""} { set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] set overrides [list {*}$overrides {*}$rawoverridecodes] @@ -3474,20 +3800,105 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu set codestack [list] } else { #append emit [lindex $o_codestack 0]$pt - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } + } + #parts ends on a pt - last code always empty string + if {$code ne ""} { + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $codestack $code] + set codestack [lremove $codestack {*}$dup_posns] + lappend codestack $code } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } } } + default { + #other ansi codes + } } - default { - if {$fullmerge} { - append emit [punk::ansi::codetype::sgr_merge [list $base {*}$codestack {*}$overrides]] $pt $R + append emit $code + } + } + return [append emit $R] + } else { + return $base$text$R + } + } + proc ansiwrap_raw {rawansi rawresets rawoverrides text} { + set codelists "" + set R "" + set overrides "" + #note that the result of any sgr_merge or sgr_merge_singles is not necessarily a single Ansi escape sequence. + #there can be SGR unmergeables (due to enhanced underlines) as well as non SGR codes + set codes [concat {*}$codelists] ;#flatten + set base [a+ {*}$codes] + set baselist [punk::ansi::ta::get_codes_single $base] + if {$rawansi ne ""} { + set rawcodes [punk::ansi::ta::get_codes_single $rawansi] ;#caller may have supplied as [a+ xxx][a+ yyy] + set base [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$rawcodes]] + set baselist [punk::ansi::ta::get_codes_single $base] + } + if {$rawresets ne ""} { + set rawresetcodes [punk::ansi::ta::get_codes_single $rawresets] + set Rcodes [punk::ansi::ta::get_codes_single $R] + set R [punk::ansi::codetype::sgr_merge_singles [list {*}$Rcodes {*}$rawresetcodes]] + } + + if {$rawoverrides ne ""} { + set rawoverridecodes [punk::ansi::ta::get_codes_single $rawoverrides] + set overrides [list {*}$overrides {*}$rawoverridecodes] + } + + set codestack [list] + if {[punk::ansi::ta::detect $text]} { + set emit "" + #set parts [punk::ansi::ta::split_codes $text] + set parts [punk::ansi::ta::split_codes_single $text] + foreach {pt code} $parts { + switch -- [llength $codestack] { + 0 { + append emit $base $pt $R + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $codestack 0]]} { + append emit $base $pt $R + set codestack [list] } else { - append emit [punk::ansi::codetype::sgr_merge_singles [list $base {*}$codestack {*}$overrides]] $pt $R + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R } } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$baselist {*}$codestack {*}$overrides]] $pt $R + } } #parts ends on a pt - last code always empty string if {$code ne ""} { @@ -3533,6 +3944,7 @@ Brightblack 100 Brightred 101 Brightgreen 102 Brightyellow 103 Brightblu } else { return $base$text$R } + } proc ansiwrap_naive {codes text} { return [a_ {*}$codes]$text[a] @@ -4481,6 +4893,20 @@ to 223 (=255 - 32) } #ever so slightly slower on short strings - much faster than split_at_codes version for large/complex ansi blocks + lappend PUNKARGS [list { + @id -id ::punk::ansi::ansistrip + @cmd -name punk::ansi::ansistrip\ + -summary\ + "Strip ANSI codes and convert VT100 graphics to unicode equivalents."\ + -help\ + "Returns a string with ANSI codes such as SGR, movements etc stripped out. + Alternate graphics chars (VT100 graphics) are replaced with modern unicode + equivalents (e.g boxdrawing glyphs). + PM, APC, SOS contents are stripped - whether or not such wrapped strings + are displayed on various terminals." + @values -min 1 -max 1 + text -type string + }] proc ansistrip {text} { #*** !doctools #[call [fun ansistrip] [arg text] ] @@ -7586,7 +8012,7 @@ tcl::namespace::eval punk::ansi::ansistring { #return pair of column extents occupied by the character index supplied. #single-width grapheme will return pair of integers of equal value - #doulbe-width grapheme will return a pair of consecutive indices + #double-width grapheme will return a pair of consecutive indices proc INDEXCOLUMNS {string idx} { #There is an index per grapheme - whether it is 1 or 2 columns wide set index [lindex [INDEXABSOLUTE $string $idx] 0] @@ -7755,6 +8181,31 @@ namespace eval punk::ansi::colour { } punk::assertion::active on + + #see also the tk function + #winfo rgb . |#XXXXXX|#XXX + #(example in punk::ansi::colourmap::get_rgb_using_tk) + + #proc percent2rgb {n} { + # # map 0..100 to a red-yellow-green sequence + # set n [expr {$n < 0? 0: $n > 100? 100: $n}] + # set red [expr {$n > 75? 60 - ($n * 15 / 25) : 15}] + # set green [expr {$n < 50? $n * 15 / 50 : 15}] + # format "#%01x%01x0" $red $green + #} ;#courtesy of RS (from tcl wiki) + proc percent2#rgb {n} { + # map 0..100 to a red-yellow-green sequence + set n [expr {$n < 0? 0: $n > 100? 100: $n}] + set red [expr {$n > 75? 1020 - ($n * 255 / 25) : 255}] + set green [expr {$n < 50? $n * 255 / 50 : 255}] + format "#%02x%02x00" $red $green + } + + proc random#rgb {} { + format #%06x [expr {int(rand() * 0xFFFFFF)}] + } + + #see also colors package #https://sourceforge.net/p/irrational-numbers/code/HEAD/tree/pkgs/Colors/trunk/colors.tcl#l159 diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm new file mode 100644 index 00000000..6e8e28e4 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ansi/colourmap-0.1.0.tm @@ -0,0 +1,966 @@ +# -*- tcl -*- +# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from -buildversion.txt +# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.3.tm +# +# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. +# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# (C) 2025 +# +# @@ Meta Begin +# Application ::punk::ansi::colourmap 0.1.0 +# Meta platform tcl +# Meta license MIT +# @@ Meta End + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# doctools header +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +#*** !doctools +#[manpage_begin shellspy_module_::punk::ansi::colourmap 0 0.1.0] +#[copyright "2025"] +#[titledesc {Module API}] [comment {-- Name section and table of contents description --}] +#[moddesc {-}] [comment {-- Description at end of page heading --}] +#[require ::punk::ansi::colourmap] +#[keywords module] +#[description] +#[para] - + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section Overview] +#[para] overview of ::punk::ansi::colourmap +#[subsection Concepts] +#[para] - + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[subsection dependencies] +#[para] packages used by ::punk::ansi::colourmap +#[list_begin itemized] + +package require Tcl 8.6- +#*** !doctools +#[item] [package {Tcl 8.6}] + + +#*** !doctools +#[list_end] + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + +#*** !doctools +#[section API] + + +tcl::namespace::eval ::punk::ansi::colourmap { + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + # Base namespace + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap}] + #[para] Core API functions for ::punk::ansi::colourmap + #[list_begin definitions] + + variable PUNKARGS + + #---------------------------------------------- + #todo - document vars as part of package API + #- or provide a function to return varnames? + #- or wrap each in a function and see if any performance/memory impact? (readonly - so should just be a reference without any copying?) + #TK_colour_map + #TK_colour_map_lookup + #TK_colour_map_merge + #TK_colour_map_reverse + #---------------------------------------------- + + + + #significantly slower than tables - but here as a check/test + lappend PUNKARGS [list { + @id -id ::punk::ansi::colourmap::get_rgb_using_tk + @cmd -name punk::ansi::colourmap::get_rgb_using_tk -help\ + "This function requires Tk to function, and will call + 'package require tk' to load it. + The name argument accepts Tk colour names or hex values + in either #XXX or #XXXXXX format. + Tk colour names can be displayed using the command: + punk::ansi::a? tk ?glob..? + + get_rgb_using_tk returns a decimal rgb string delimited with dashes. + e.g + get_rgb_using_tk #FFF + 255-255-255 + get_rgb_using_tk SlateBlue + 106-90-205" + @leaders + name -type string|stringstartswith(#) + }] + proc get_rgb_using_tk {name} { + package require tk + #assuming 'winfo depth .' is always 32 ? + set RGB [winfo rgb . $name] + set rgb [lmap n $RGB {expr {$n / 256}}] + return [join $rgb -] + } + + variable TK_colour_map + tcl::dict::set TK_colour_map "alice blue" 240-248-255 + tcl::dict::set TK_colour_map AliceBlue 240-248-255 + tcl::dict::set TK_colour_map "antique white" 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite 250-235-215 + tcl::dict::set TK_colour_map AntiqueWhite1 255-239-219 + tcl::dict::set TK_colour_map AntiqueWhite2 238-223-204 + tcl::dict::set TK_colour_map AntiqueWhite3 205-192-176 + tcl::dict::set TK_colour_map AntiqueWhite4 139-131-120 + tcl::dict::set TK_colour_map aqua 0-255-255 + tcl::dict::set TK_colour_map aquamarine 127-255-212 + tcl::dict::set TK_colour_map aquamarine1 127-255-212 + tcl::dict::set TK_colour_map aquamarine2 118-238-198 + tcl::dict::set TK_colour_map aquamarine3 102-205-170 + tcl::dict::set TK_colour_map aquamarine4 69-139-16 + tcl::dict::set TK_colour_map azure 240-255-255 + tcl::dict::set TK_colour_map azure1 240-255-255 + tcl::dict::set TK_colour_map azure2 224-238-238 + tcl::dict::set TK_colour_map azure3 193-205-205 + tcl::dict::set TK_colour_map azure4 131-139-139 + tcl::dict::set TK_colour_map beige 245-245-220 + tcl::dict::set TK_colour_map bisque 255-228-196 + tcl::dict::set TK_colour_map bisque1 255-228-196 + tcl::dict::set TK_colour_map bisque2 238-213-183 + tcl::dict::set TK_colour_map bisque3 205-183-158 + tcl::dict::set TK_colour_map bisque4 139-125-107 + tcl::dict::set TK_colour_map black 0-0-0 + tcl::dict::set TK_colour_map "blanched almond" 255-235-205 + tcl::dict::set TK_colour_map BlanchedAlmond 255-235-205 + tcl::dict::set TK_colour_map blue 0-0-255 + tcl::dict::set TK_colour_map "blue violet" 138-43-226 + tcl::dict::set TK_colour_map blue1 0-0-255 + tcl::dict::set TK_colour_map blue2 0-0-238 + tcl::dict::set TK_colour_map blue3 0-0-205 + tcl::dict::set TK_colour_map blue4 0-0-139 + tcl::dict::set TK_colour_map BlueViolet 138-43-226 + tcl::dict::set TK_colour_map brown 165-42-42 + tcl::dict::set TK_colour_map brown1 255-64-64 + tcl::dict::set TK_colour_map brown2 238-59-59 + tcl::dict::set TK_colour_map brown3 205-51-51 + tcl::dict::set TK_colour_map brown4 139-35-35 + tcl::dict::set TK_colour_map burlywood 222-184-135 + tcl::dict::set TK_colour_map burlywood1 255-211-155 + tcl::dict::set TK_colour_map burlywood2 238-197-145 + tcl::dict::set TK_colour_map burlywood3 205-170-125 + tcl::dict::set TK_colour_map burlywood4 139-115-85 + tcl::dict::set TK_colour_map "cadet blue" 95-158-160 + tcl::dict::set TK_colour_map CadetBlue 95-158-160 + tcl::dict::set TK_colour_map CadetBlue1 152-245-255 + tcl::dict::set TK_colour_map CadetBlue2 142-229-238 + tcl::dict::set TK_colour_map CadetBlue3 122-197-205 + tcl::dict::set TK_colour_map CadetBlue4 83-134-139 + tcl::dict::set TK_colour_map chartreuse 127-255-0 + tcl::dict::set TK_colour_map chartreuse1 127-255-0 + tcl::dict::set TK_colour_map chartreuse2 118-238-0 + tcl::dict::set TK_colour_map chartreuse3 102-205-0 + tcl::dict::set TK_colour_map chartreuse4 69-139-0 + tcl::dict::set TK_colour_map chocolate 210-105-30 + tcl::dict::set TK_colour_map chocolate1 255-127-36 + tcl::dict::set TK_colour_map chocolate2 238-118-33 + tcl::dict::set TK_colour_map chocolate3 205-102-29 + tcl::dict::set TK_colour_map chocolate4 139-69-19 + tcl::dict::set TK_colour_map coral 255-127-80 + tcl::dict::set TK_colour_map coral1 255-114-86 + tcl::dict::set TK_colour_map coral2 238-106-80 + tcl::dict::set TK_colour_map coral3 205-91-69 + tcl::dict::set TK_colour_map coral4 139-62-47 + tcl::dict::set TK_colour_map "cornflower blue" 100-149-237 + tcl::dict::set TK_colour_map CornflowerBlue 100-149-237 + tcl::dict::set TK_colour_map cornsilk 255-248-220 + tcl::dict::set TK_colour_map cornsilk1 255-248-220 + tcl::dict::set TK_colour_map cornsilk2 238-232-205 + tcl::dict::set TK_colour_map cornsilk3 205-200-177 + tcl::dict::set TK_colour_map cornsilk4 139-136-120 + tcl::dict::set TK_colour_map crimson 220-20-60 + tcl::dict::set TK_colour_map cyan 0-255-255 + tcl::dict::set TK_colour_map cyan1 0-255-255 + tcl::dict::set TK_colour_map cyan2 0-238-238 + tcl::dict::set TK_colour_map cyan3 0-205-205 + tcl::dict::set TK_colour_map cyan4 0-139-139 + tcl::dict::set TK_colour_map "dark blue" 0-0-139 + tcl::dict::set TK_colour_map "dark cyan" 0-139-139 + tcl::dict::set TK_colour_map "dark goldenrod" 184-134-11 + tcl::dict::set TK_colour_map "dark gray" 169-169-169 + tcl::dict::set TK_colour_map "dark green" 0-100-0 + tcl::dict::set TK_colour_map "dark grey" 169-169-169 + tcl::dict::set TK_colour_map "dark khaki" 189-183-107 + tcl::dict::set TK_colour_map "dark magenta" 139-0-139 + tcl::dict::set TK_colour_map "dark olive green" 85-107-47 + tcl::dict::set TK_colour_map "dark orange" 255-140-0 + tcl::dict::set TK_colour_map "dark orchid" 153-50-204 + tcl::dict::set TK_colour_map "dark red" 139-0-0 + tcl::dict::set TK_colour_map "dark salmon" 233-150-122 + tcl::dict::set TK_colour_map "dark sea green" 143-188-143 + tcl::dict::set TK_colour_map "dark slate blue" 72-61-139 + tcl::dict::set TK_colour_map "dark slate gray" 47-79-79 + tcl::dict::set TK_colour_map "dark slate grey" 47-79-79 + tcl::dict::set TK_colour_map "dark turquoise" 0-206-209 + tcl::dict::set TK_colour_map "dark violet" 148-0-211 + tcl::dict::set TK_colour_map DarkBlue 0-0-139 + tcl::dict::set TK_colour_map DarkCyan 0-139-139 + tcl::dict::set TK_colour_map DarkGoldenrod 184-134-11 + tcl::dict::set TK_colour_map DarkGoldenrod1 255-185-15 + tcl::dict::set TK_colour_map DarkGoldenrod2 238-173-14 + tcl::dict::set TK_colour_map DarkGoldenrod3 205-149-12 + tcl::dict::set TK_colour_map DarkGoldenrod4 139-101-8 + tcl::dict::set TK_colour_map DarkGray 169-169-169 + tcl::dict::set TK_colour_map DarkGreen 0-100-0 + tcl::dict::set TK_colour_map DarkGrey 169-169-169 + tcl::dict::set TK_colour_map DarkKhaki 189-183-107 + tcl::dict::set TK_colour_map DarkMagenta 139-0-139 + tcl::dict::set TK_colour_map DarkOliveGreen 85-107-47 + tcl::dict::set TK_colour_map DarkOliveGreen1 202-255-112 + tcl::dict::set TK_colour_map DarkOliveGreen2 188-238-104 + tcl::dict::set TK_colour_map DarkOliveGreen3 162-205-90 + tcl::dict::set TK_colour_map DarkOliveGreen4 110-139-61 + tcl::dict::set TK_colour_map DarkOrange 255-140-0 + tcl::dict::set TK_colour_map DarkOrange1 255-127-0 + tcl::dict::set TK_colour_map DarkOrange2 238-118-0 + tcl::dict::set TK_colour_map DarkOrange3 205-102-0 + tcl::dict::set TK_colour_map DarkOrange4 139-69-0 + tcl::dict::set TK_colour_map DarkOrchid 153-50-204 + tcl::dict::set TK_colour_map DarkOrchid1 191-62-255 + tcl::dict::set TK_colour_map DarkOrchid2 178-58-238 + tcl::dict::set TK_colour_map DarkOrchid3 154-50-205 + tcl::dict::set TK_colour_map DarkOrchid4 104-34-139 + tcl::dict::set TK_colour_map DarkRed 139-0-0 + tcl::dict::set TK_colour_map DarkSalmon 233-150-122 + tcl::dict::set TK_colour_map DarkSeaGreen 43-188-143 + tcl::dict::set TK_colour_map DarkSeaGreen1 193-255-193 + tcl::dict::set TK_colour_map DarkSeaGreen2 180-238-180 + tcl::dict::set TK_colour_map DarkSeaGreen3 155-205-155 + tcl::dict::set TK_colour_map DarkSeaGreen4 105-139-105 + tcl::dict::set TK_colour_map DarkSlateBlue 72-61-139 + tcl::dict::set TK_colour_map DarkSlateGray 47-79-79 + tcl::dict::set TK_colour_map DarkSlateGray1 151-255-255 + tcl::dict::set TK_colour_map DarkSlateGray2 141-238-238 + tcl::dict::set TK_colour_map DarkSlateGray3 121-205-205 + tcl::dict::set TK_colour_map DarkSlateGray4 82-139-139 + tcl::dict::set TK_colour_map DarkSlateGrey 47-79-79 + tcl::dict::set TK_colour_map DarkTurquoise 0-206-209 + tcl::dict::set TK_colour_map DarkViolet 148-0-211 + tcl::dict::set TK_colour_map "deep pink" 255-20-147 + tcl::dict::set TK_colour_map "deep sky blue" 0-191-255 + tcl::dict::set TK_colour_map DeepPink 255-20-147 + tcl::dict::set TK_colour_map DeepPink1 255-20-147 + tcl::dict::set TK_colour_map DeepPink2 238-18-137 + tcl::dict::set TK_colour_map DeepPink3 205-16-118 + tcl::dict::set TK_colour_map DeepPink4 139-10-80 + tcl::dict::set TK_colour_map DeepSkyBlue 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue1 0-191-255 + tcl::dict::set TK_colour_map DeepSkyBlue2 0-178-238 + tcl::dict::set TK_colour_map DeepSkyBlue3 0-154-205 + tcl::dict::set TK_colour_map DeepSkyBlue4 0-104-139 + tcl::dict::set TK_colour_map "dim gray" 105-105-105 + tcl::dict::set TK_colour_map "dim grey" 105-105-105 + tcl::dict::set TK_colour_map DimGray 105-105-105 + tcl::dict::set TK_colour_map DimGrey 105-105-105 + tcl::dict::set TK_colour_map "dodger blue" 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue1 30-144-255 + tcl::dict::set TK_colour_map DodgerBlue2 28-134-238 + tcl::dict::set TK_colour_map DodgerBlue3 24-116-205 + tcl::dict::set TK_colour_map DodgerBlue4 16-78-139 + tcl::dict::set TK_colour_map firebrick 178-34-34 + tcl::dict::set TK_colour_map firebrick1 255-48-48 + tcl::dict::set TK_colour_map firebrick2 238-44-44 + tcl::dict::set TK_colour_map firebrick3 205-38-38 + tcl::dict::set TK_colour_map firebrick4 139-26-26 + tcl::dict::set TK_colour_map "floral white" 255-250-240 + tcl::dict::set TK_colour_map FloralWhite 255-250-240 + tcl::dict::set TK_colour_map "forest green" 34-139-34 + tcl::dict::set TK_colour_map ForestGreen 34-139-34 + tcl::dict::set TK_colour_map fuchsia 255-0-255 + tcl::dict::set TK_colour_map gainsboro 220-220-220 + tcl::dict::set TK_colour_map "ghost white" 248-248-255 + tcl::dict::set TK_colour_map GhostWhite 248-248-255 + tcl::dict::set TK_colour_map gold 255-215-0 + tcl::dict::set TK_colour_map gold1 255-215-0 + tcl::dict::set TK_colour_map gold2 238-201-0 + tcl::dict::set TK_colour_map gold3 205-173-0 + tcl::dict::set TK_colour_map gold4 139-117-0 + tcl::dict::set TK_colour_map goldenrod 218-165-32 + tcl::dict::set TK_colour_map goldenrod1 255-193-37 + tcl::dict::set TK_colour_map goldenrod2 238-180-34 + tcl::dict::set TK_colour_map goldenrod3 205-155-29 + tcl::dict::set TK_colour_map goldenrod4 139-105-20 + tcl::dict::set TK_colour_map gray 128-128-128 + tcl::dict::set TK_colour_map gray0 0-0-0 + tcl::dict::set TK_colour_map gray1 3-3-3 + tcl::dict::set TK_colour_map gray2 5-5-5 + tcl::dict::set TK_colour_map gray3 8-8-8 + tcl::dict::set TK_colour_map gray4 10-10-10 + tcl::dict::set TK_colour_map gray5 13-13-13 + tcl::dict::set TK_colour_map gray6 15-15-15 + tcl::dict::set TK_colour_map gray7 18-18-18 + tcl::dict::set TK_colour_map gray8 20-20-20 + tcl::dict::set TK_colour_map gray9 23-23-23 + tcl::dict::set TK_colour_map gray10 26-26-26 + tcl::dict::set TK_colour_map gray11 28-28-28 + tcl::dict::set TK_colour_map gray12 31-31-31 + tcl::dict::set TK_colour_map gray13 33-33-33 + tcl::dict::set TK_colour_map gray14 36-36-36 + tcl::dict::set TK_colour_map gray15 38-38-38 + tcl::dict::set TK_colour_map gray16 41-41-41 + tcl::dict::set TK_colour_map gray17 43-43-43 + tcl::dict::set TK_colour_map gray18 46-46-46 + tcl::dict::set TK_colour_map gray19 48-48-48 + tcl::dict::set TK_colour_map gray20 51-51-51 + tcl::dict::set TK_colour_map gray21 54-54-54 + tcl::dict::set TK_colour_map gray22 56-56-56 + tcl::dict::set TK_colour_map gray23 59-59-59 + tcl::dict::set TK_colour_map gray24 61-61-61 + tcl::dict::set TK_colour_map gray25 64-64-64 + tcl::dict::set TK_colour_map gray26 66-66-66 + tcl::dict::set TK_colour_map gray27 69-69-69 + tcl::dict::set TK_colour_map gray28 71-71-71 + tcl::dict::set TK_colour_map gray29 74-74-74 + tcl::dict::set TK_colour_map gray30 77-77-77 + tcl::dict::set TK_colour_map gray31 79-79-79 + tcl::dict::set TK_colour_map gray32 82-82-82 + tcl::dict::set TK_colour_map gray33 84-84-84 + tcl::dict::set TK_colour_map gray34 87-87-87 + tcl::dict::set TK_colour_map gray35 89-89-89 + tcl::dict::set TK_colour_map gray36 92-92-92 + tcl::dict::set TK_colour_map gray37 94-94-94 + tcl::dict::set TK_colour_map gray38 97-97-97 + tcl::dict::set TK_colour_map gray39 99-99-99 + tcl::dict::set TK_colour_map gray40 102-102-102 + tcl::dict::set TK_colour_map gray41 105-105-105 + tcl::dict::set TK_colour_map gray42 107-107-107 + tcl::dict::set TK_colour_map gray43 110-110-110 + tcl::dict::set TK_colour_map gray44 112-112-112 + tcl::dict::set TK_colour_map gray45 115-115-115 + tcl::dict::set TK_colour_map gray46 117-117-117 + tcl::dict::set TK_colour_map gray47 120-120-120 + tcl::dict::set TK_colour_map gray48 122-122-122 + tcl::dict::set TK_colour_map gray49 125-125-125 + tcl::dict::set TK_colour_map gray50 127-127-127 + tcl::dict::set TK_colour_map gray51 130-130-130 + tcl::dict::set TK_colour_map gray52 133-133-133 + tcl::dict::set TK_colour_map gray53 135-135-135 + tcl::dict::set TK_colour_map gray54 138-138-138 + tcl::dict::set TK_colour_map gray55 140-140-140 + tcl::dict::set TK_colour_map gray56 143-143-143 + tcl::dict::set TK_colour_map gray57 145-145-145 + tcl::dict::set TK_colour_map gray58 148-148-148 + tcl::dict::set TK_colour_map gray59 150-150-150 + tcl::dict::set TK_colour_map gray60 153-153-153 + tcl::dict::set TK_colour_map gray61 156-156-156 + tcl::dict::set TK_colour_map gray62 158-158-158 + tcl::dict::set TK_colour_map gray63 161-161-161 + tcl::dict::set TK_colour_map gray64 163-163-163 + tcl::dict::set TK_colour_map gray65 166-166-166 + tcl::dict::set TK_colour_map gray66 168-168-168 + tcl::dict::set TK_colour_map gray67 171-171-171 + tcl::dict::set TK_colour_map gray68 173-173-173 + tcl::dict::set TK_colour_map gray69 176-176-176 + tcl::dict::set TK_colour_map gray70 179-179-179 + tcl::dict::set TK_colour_map gray71 181-181-181 + tcl::dict::set TK_colour_map gray72 184-184-184 + tcl::dict::set TK_colour_map gray73 186-186-186 + tcl::dict::set TK_colour_map gray74 189-189-189 + tcl::dict::set TK_colour_map gray75 191-191-191 + tcl::dict::set TK_colour_map gray76 194-194-194 + tcl::dict::set TK_colour_map gray77 196-196-196 + tcl::dict::set TK_colour_map gray78 199-199-199 + tcl::dict::set TK_colour_map gray79 201-201-201 + tcl::dict::set TK_colour_map gray80 204-204-204 + tcl::dict::set TK_colour_map gray81 207-207-207 + tcl::dict::set TK_colour_map gray82 209-209-209 + tcl::dict::set TK_colour_map gray83 212-212-212 + tcl::dict::set TK_colour_map gray84 214-214-214 + tcl::dict::set TK_colour_map gray85 217-217-217 + tcl::dict::set TK_colour_map gray86 219-219-219 + tcl::dict::set TK_colour_map gray87 222-222-222 + tcl::dict::set TK_colour_map gray88 224-224-224 + tcl::dict::set TK_colour_map gray89 227-227-227 + tcl::dict::set TK_colour_map gray90 229-229-229 + tcl::dict::set TK_colour_map gray91 232-232-232 + tcl::dict::set TK_colour_map gray92 235-235-235 + tcl::dict::set TK_colour_map gray93 237-237-237 + tcl::dict::set TK_colour_map gray94 240-240-240 + tcl::dict::set TK_colour_map gray95 242-242-242 + tcl::dict::set TK_colour_map gray96 245-245-245 + tcl::dict::set TK_colour_map gray97 247-247-247 + tcl::dict::set TK_colour_map gray98 250-250-250 + tcl::dict::set TK_colour_map gray99 252-252-252 + tcl::dict::set TK_colour_map gray100 255-255-255 + tcl::dict::set TK_colour_map green 0-128-0 + tcl::dict::set TK_colour_map "green yellow" 173-255-47 + tcl::dict::set TK_colour_map green1 0-255-0 + tcl::dict::set TK_colour_map green2 0-238-0 + tcl::dict::set TK_colour_map green3 0-205-0 + tcl::dict::set TK_colour_map green4 0-139-0 + tcl::dict::set TK_colour_map GreenYellow 173-255-47 + tcl::dict::set TK_colour_map grey 128-128-128 + tcl::dict::set TK_colour_map grey0 0-0-0 + tcl::dict::set TK_colour_map grey1 3-3-3 + tcl::dict::set TK_colour_map grey2 5-5-5 + tcl::dict::set TK_colour_map grey3 8-8-8 + tcl::dict::set TK_colour_map grey4 10-10-10 + tcl::dict::set TK_colour_map grey5 13-13-13 + tcl::dict::set TK_colour_map grey6 15-15-15 + tcl::dict::set TK_colour_map grey7 18-18-18 + tcl::dict::set TK_colour_map grey8 20-20-20 + tcl::dict::set TK_colour_map grey9 23-23-23 + tcl::dict::set TK_colour_map grey10 26-26-26 + tcl::dict::set TK_colour_map grey11 28-28-28 + tcl::dict::set TK_colour_map grey12 31-31-31 + tcl::dict::set TK_colour_map grey13 33-33-33 + tcl::dict::set TK_colour_map grey14 36-36-36 + tcl::dict::set TK_colour_map grey15 38-38-38 + tcl::dict::set TK_colour_map grey16 41-41-41 + tcl::dict::set TK_colour_map grey17 43-43-43 + tcl::dict::set TK_colour_map grey18 46-46-46 + tcl::dict::set TK_colour_map grey19 48-48-48 + tcl::dict::set TK_colour_map grey20 51-51-51 + tcl::dict::set TK_colour_map grey21 54-54-54 + tcl::dict::set TK_colour_map grey22 56-56-56 + tcl::dict::set TK_colour_map grey23 59-59-59 + tcl::dict::set TK_colour_map grey24 61-61-61 + tcl::dict::set TK_colour_map grey25 64-64-64 + tcl::dict::set TK_colour_map grey26 66-66-66 + tcl::dict::set TK_colour_map grey27 69-69-69 + tcl::dict::set TK_colour_map grey28 71-71-71 + tcl::dict::set TK_colour_map grey29 74-74-74 + tcl::dict::set TK_colour_map grey30 77-77-77 + tcl::dict::set TK_colour_map grey31 79-79-79 + tcl::dict::set TK_colour_map grey32 82-82-82 + tcl::dict::set TK_colour_map grey33 84-84-84 + tcl::dict::set TK_colour_map grey34 87-87-87 + tcl::dict::set TK_colour_map grey35 89-89-89 + tcl::dict::set TK_colour_map grey36 92-92-92 + tcl::dict::set TK_colour_map grey37 94-94-94 + tcl::dict::set TK_colour_map grey38 97-97-97 + tcl::dict::set TK_colour_map grey39 99-99-99 + tcl::dict::set TK_colour_map grey40 102-102-102 + tcl::dict::set TK_colour_map grey41 105-105-105 + tcl::dict::set TK_colour_map grey42 107-107-107 + tcl::dict::set TK_colour_map grey43 110-110-110 + tcl::dict::set TK_colour_map grey44 112-112-112 + tcl::dict::set TK_colour_map grey45 115-115-115 + tcl::dict::set TK_colour_map grey46 117-117-117 + tcl::dict::set TK_colour_map grey47 120-120-120 + tcl::dict::set TK_colour_map grey48 122-122-122 + tcl::dict::set TK_colour_map grey49 125-125-125 + tcl::dict::set TK_colour_map grey50 127-127-127 + tcl::dict::set TK_colour_map grey51 130-130-130 + tcl::dict::set TK_colour_map grey52 133-133-133 + tcl::dict::set TK_colour_map grey53 135-135-135 + tcl::dict::set TK_colour_map grey54 138-138-138 + tcl::dict::set TK_colour_map grey55 140-140-140 + tcl::dict::set TK_colour_map grey56 143-143-143 + tcl::dict::set TK_colour_map grey57 145-145-145 + tcl::dict::set TK_colour_map grey58 148-148-148 + tcl::dict::set TK_colour_map grey59 150-150-150 + tcl::dict::set TK_colour_map grey60 153-153-153 + tcl::dict::set TK_colour_map grey61 156-156-156 + tcl::dict::set TK_colour_map grey62 158-158-158 + tcl::dict::set TK_colour_map grey63 161-161-161 + tcl::dict::set TK_colour_map grey64 163-163-163 + tcl::dict::set TK_colour_map grey65 166-166-166 + tcl::dict::set TK_colour_map grey66 168-168-168 + tcl::dict::set TK_colour_map grey67 171-171-171 + tcl::dict::set TK_colour_map grey68 173-173-173 + tcl::dict::set TK_colour_map grey69 176-176-176 + tcl::dict::set TK_colour_map grey70 179-179-179 + tcl::dict::set TK_colour_map grey71 181-181-181 + tcl::dict::set TK_colour_map grey72 184-184-184 + tcl::dict::set TK_colour_map grey73 186-186-186 + tcl::dict::set TK_colour_map grey74 189-189-189 + tcl::dict::set TK_colour_map grey75 191-191-191 + tcl::dict::set TK_colour_map grey76 194-194-194 + tcl::dict::set TK_colour_map grey77 196-196-196 + tcl::dict::set TK_colour_map grey78 199-199-199 + tcl::dict::set TK_colour_map grey79 201-201-201 + tcl::dict::set TK_colour_map grey80 204-204-204 + tcl::dict::set TK_colour_map grey81 207-207-207 + tcl::dict::set TK_colour_map grey82 209-209-209 + tcl::dict::set TK_colour_map grey83 212-212-212 + tcl::dict::set TK_colour_map grey84 214-214-214 + tcl::dict::set TK_colour_map grey85 217-217-217 + tcl::dict::set TK_colour_map grey86 219-219-219 + tcl::dict::set TK_colour_map grey87 222-222-222 + tcl::dict::set TK_colour_map grey88 224-224-224 + tcl::dict::set TK_colour_map grey89 227-227-227 + tcl::dict::set TK_colour_map grey90 229-229-229 + tcl::dict::set TK_colour_map grey91 232-232-232 + tcl::dict::set TK_colour_map grey92 235-235-235 + tcl::dict::set TK_colour_map grey93 237-237-237 + tcl::dict::set TK_colour_map grey94 240-240-240 + tcl::dict::set TK_colour_map grey95 242-242-242 + tcl::dict::set TK_colour_map grey96 245-245-245 + tcl::dict::set TK_colour_map grey97 247-247-247 + tcl::dict::set TK_colour_map grey98 250-250-250 + tcl::dict::set TK_colour_map grey99 252-252-252 + tcl::dict::set TK_colour_map grey100 255-255-255 + tcl::dict::set TK_colour_map honeydew 240-255-240 + tcl::dict::set TK_colour_map honeydew1 240-255-240 + tcl::dict::set TK_colour_map honeydew2 224-238-224 + tcl::dict::set TK_colour_map honeydew3 193-205-193 + tcl::dict::set TK_colour_map honeydew4 131-139-131 + tcl::dict::set TK_colour_map "hot pink" 255-105-180 + tcl::dict::set TK_colour_map HotPink 255-105-180 + tcl::dict::set TK_colour_map HotPink1 255-110-180 + tcl::dict::set TK_colour_map HotPink2 238-106-167 + tcl::dict::set TK_colour_map HotPink3 205-96-144 + tcl::dict::set TK_colour_map HotPink4 139-58-98 + tcl::dict::set TK_colour_map "indian red" 205-92-92 + tcl::dict::set TK_colour_map IndianRed 205-92-92 + tcl::dict::set TK_colour_map IndianRed1 255-106-106 + tcl::dict::set TK_colour_map IndianRed2 238-99-99 + tcl::dict::set TK_colour_map IndianRed3 205-85-85 + tcl::dict::set TK_colour_map IndianRed4 139-58-58 + tcl::dict::set TK_colour_map indigo 75-0-130 + tcl::dict::set TK_colour_map ivory 255-255-240 + tcl::dict::set TK_colour_map ivory1 255-255-240 + tcl::dict::set TK_colour_map ivory2 238-238-224 + tcl::dict::set TK_colour_map ivory3 205-205-193 + tcl::dict::set TK_colour_map ivory4 139-139-131 + tcl::dict::set TK_colour_map khaki 240-230-140 + tcl::dict::set TK_colour_map khaki1 255-246-143 + tcl::dict::set TK_colour_map khaki2 238-230-133 + tcl::dict::set TK_colour_map khaki3 205-198-115 + tcl::dict::set TK_colour_map khaki4 139-134-78 + tcl::dict::set TK_colour_map lavender 230-230-250 + tcl::dict::set TK_colour_map "lavender blush" 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush1 255-240-245 + tcl::dict::set TK_colour_map LavenderBlush2 238-224-229 + tcl::dict::set TK_colour_map LavenderBlush3 205-193-197 + tcl::dict::set TK_colour_map LavenderBlush4 139-131-134 + tcl::dict::set TK_colour_map "lawn green" 124-252-0 + tcl::dict::set TK_colour_map LawnGreen 124-252-0 + tcl::dict::set TK_colour_map "lemon chiffon" 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon1 255-250-205 + tcl::dict::set TK_colour_map LemonChiffon2 238-233-191 + tcl::dict::set TK_colour_map LemonChiffon3 205-201-165 + tcl::dict::set TK_colour_map LemonChiffon4 139-137-112 + tcl::dict::set TK_colour_map "light blue" 173-216-230 + tcl::dict::set TK_colour_map "light coral" 240-128-128 + tcl::dict::set TK_colour_map "light cyan" 224-255-255 + tcl::dict::set TK_colour_map "light goldenrod" 238-221-130 + tcl::dict::set TK_colour_map "light goldenrod yellow" 250-250-210 + tcl::dict::set TK_colour_map "light gray" 211-211-211 + tcl::dict::set TK_colour_map "light green" 144-238-144 + tcl::dict::set TK_colour_map "light grey" 211-211-211 + tcl::dict::set TK_colour_map "light pink" 255-182-193 + tcl::dict::set TK_colour_map "light salmon" 255-160-122 + tcl::dict::set TK_colour_map "light sea green" 32-178-170 + tcl::dict::set TK_colour_map "light sky blue" 135-206-250 + tcl::dict::set TK_colour_map "light slate blue" 132-112-255 + tcl::dict::set TK_colour_map "light slate gray" 119-136-153 + tcl::dict::set TK_colour_map "light slate grey" 119-136-153 + tcl::dict::set TK_colour_map "light steel blue" 176-196-222 + tcl::dict::set TK_colour_map "light yellow" 255-255-224 + tcl::dict::set TK_colour_map LightBlue 173-216-230 + tcl::dict::set TK_colour_map LightBlue1 191-239-255 + tcl::dict::set TK_colour_map LightBlue2 178-223-238 + tcl::dict::set TK_colour_map LightBlue3 154-192-205 + tcl::dict::set TK_colour_map LightBlue4 104-131-139 + tcl::dict::set TK_colour_map LightCoral 240-128-128 + tcl::dict::set TK_colour_map LightCyan 224-255-255 + tcl::dict::set TK_colour_map LightCyan1 224-255-255 + tcl::dict::set TK_colour_map LightCyan2 209-238-238 + tcl::dict::set TK_colour_map LightCyan3 180-205-205 + tcl::dict::set TK_colour_map LightCyan4 122-139-139 + tcl::dict::set TK_colour_map LightGoldenrod 238-221-130 + tcl::dict::set TK_colour_map LightGoldenrod1 255-236-139 + tcl::dict::set TK_colour_map LightGoldenrod2 238-220-130 + tcl::dict::set TK_colour_map LightGoldenrod3 205-190-112 + tcl::dict::set TK_colour_map LightGoldenrod4 139-129-76 + tcl::dict::set TK_colour_map LightGoldenrodYellow 250-250-210 + tcl::dict::set TK_colour_map LightGray 211-211-211 + tcl::dict::set TK_colour_map LightGreen 144-238-144 + tcl::dict::set TK_colour_map LightGrey 211-211-211 + tcl::dict::set TK_colour_map LightPink 255-182-193 + tcl::dict::set TK_colour_map LightPink1 255-174-185 + tcl::dict::set TK_colour_map LightPink2 238-162-173 + tcl::dict::set TK_colour_map LightPink3 205-140-149 + tcl::dict::set TK_colour_map LightPink4 139-95-101 + tcl::dict::set TK_colour_map LightSalmon 255-160-122 + tcl::dict::set TK_colour_map LightSalmon1 255-160-122 + tcl::dict::set TK_colour_map LightSalmon2 238-149-114 + tcl::dict::set TK_colour_map LightSalmon3 205-129-98 + tcl::dict::set TK_colour_map LightSalmon4 139-87-66 + tcl::dict::set TK_colour_map LightSeaGreen 32-178-170 + tcl::dict::set TK_colour_map LightSkyBlue 135-206-250 + tcl::dict::set TK_colour_map LightSkyBlue1 176-226-255 + tcl::dict::set TK_colour_map LightSkyBlue2 164-211-238 + tcl::dict::set TK_colour_map LightSkyBlue3 141-182-205 + tcl::dict::set TK_colour_map LightSkyBlue4 96-123-139 + tcl::dict::set TK_colour_map LightSlateBlue 132-112-255 + tcl::dict::set TK_colour_map LightSlateGray 119-136-153 + tcl::dict::set TK_colour_map LightSlateGrey 119-136-153 + tcl::dict::set TK_colour_map LightSteelBlue 176-196-222 + tcl::dict::set TK_colour_map LightSteelBlue1 202-225-255 + tcl::dict::set TK_colour_map LightSteelBlue2 188-210-238 + tcl::dict::set TK_colour_map LightSteelBlue3 162-181-205 + tcl::dict::set TK_colour_map LightSteelBlue4 110-123-139 + tcl::dict::set TK_colour_map LightYellow 255-255-224 + tcl::dict::set TK_colour_map LightYellow1 255-255-224 + tcl::dict::set TK_colour_map LightYellow2 238-238-209 + tcl::dict::set TK_colour_map LightYellow3 205-205-180 + tcl::dict::set TK_colour_map LightYellow4 139-139-122 + tcl::dict::set TK_colour_map lime 0-255-0 + tcl::dict::set TK_colour_map "lime green" 50-205-50 + tcl::dict::set TK_colour_map LimeGreen 50-205-50 + tcl::dict::set TK_colour_map linen 250-240-230 + tcl::dict::set TK_colour_map magenta 255-0-255 + tcl::dict::set TK_colour_map magenta1 255-0-255 + tcl::dict::set TK_colour_map magenta2 238-0-238 + tcl::dict::set TK_colour_map magenta3 205-0-205 + tcl::dict::set TK_colour_map magenta4 139-0-139 + tcl::dict::set TK_colour_map maroon 128-0-0 + tcl::dict::set TK_colour_map maroon1 255-52-179 + tcl::dict::set TK_colour_map maroon2 238-48-167 + tcl::dict::set TK_colour_map maroon3 205-41-144 + tcl::dict::set TK_colour_map maroon4 139-28-98 + tcl::dict::set TK_colour_map "medium aquamarine" 102-205-170 + tcl::dict::set TK_colour_map "medium blue" 0-0-205 + tcl::dict::set TK_colour_map "medium orchid" 186-85-211 + tcl::dict::set TK_colour_map "medium purple" 147-112-219 + tcl::dict::set TK_colour_map "medium sea green" 60-179-113 + tcl::dict::set TK_colour_map "medium slate blue" 123-104-238 + tcl::dict::set TK_colour_map "medium spring green" 0-250-154 + tcl::dict::set TK_colour_map "medium turquoise" 72-209-204 + tcl::dict::set TK_colour_map "medium violet red" 199-21-133 + tcl::dict::set TK_colour_map MediumAquamarine 102-205-170 + tcl::dict::set TK_colour_map MediumBlue 0-0-205 + tcl::dict::set TK_colour_map MediumOrchid 186-85-211 + tcl::dict::set TK_colour_map MediumOrchid1 224-102-255 + tcl::dict::set TK_colour_map MediumOrchid2 209-95-238 + tcl::dict::set TK_colour_map MediumOrchid3 180-82-205 + tcl::dict::set TK_colour_map MediumOrchid4 122-55-139 + tcl::dict::set TK_colour_map MediumPurple 147-112-219 + tcl::dict::set TK_colour_map MediumPurple1 171-130-255 + tcl::dict::set TK_colour_map MediumPurple2 159-121-238 + tcl::dict::set TK_colour_map MediumPurple3 137-104-205 + tcl::dict::set TK_colour_map MediumPurple4 93-71-139 + tcl::dict::set TK_colour_map MediumSeaGreen 60-179-113 + tcl::dict::set TK_colour_map MediumSlateBlue 123-104-238 + tcl::dict::set TK_colour_map MediumSpringGreen 0-250-154 + tcl::dict::set TK_colour_map MediumTurquoise 72-209-204 + tcl::dict::set TK_colour_map MediumVioletRed 199-21-133 + tcl::dict::set TK_colour_map "midnight blue" 25-25-112 + tcl::dict::set TK_colour_map MidnightBlue 25-25-112 + tcl::dict::set TK_colour_map "mint cream" 245-255-250 + tcl::dict::set TK_colour_map MintCream 245-255-250 + tcl::dict::set TK_colour_map "misty rose" 255-228-225 + tcl::dict::set TK_colour_map MistyRose 255-228-225 + tcl::dict::set TK_colour_map MistyRose1 255-228-225 + tcl::dict::set TK_colour_map MistyRose2 238-213-210 + tcl::dict::set TK_colour_map MistyRose3 205-183-181 + tcl::dict::set TK_colour_map MistyRose4 139-125-123 + tcl::dict::set TK_colour_map moccasin 255-228-181 + tcl::dict::set TK_colour_map "navajo white" 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite1 255-222-173 + tcl::dict::set TK_colour_map NavajoWhite2 238-207-161 + tcl::dict::set TK_colour_map NavajoWhite3 205-179-139 + tcl::dict::set TK_colour_map NavajoWhite4 139-121-94 + tcl::dict::set TK_colour_map navy 0-0-128 + tcl::dict::set TK_colour_map "navy blue" 0-0-128 + tcl::dict::set TK_colour_map NavyBlue 0-0-128 + tcl::dict::set TK_colour_map "old lace" 253-245-230 + tcl::dict::set TK_colour_map OldLace 253-245-230 + tcl::dict::set TK_colour_map olive 128-128-0 + tcl::dict::set TK_colour_map "olive drab" 107-142-35 + tcl::dict::set TK_colour_map OliveDrab 107-142-35 + tcl::dict::set TK_colour_map OliveDrab1 192-255-62 + tcl::dict::set TK_colour_map OliveDrab2 179-238-58 + tcl::dict::set TK_colour_map OliveDrab3 154-205-50 + tcl::dict::set TK_colour_map OliveDrab4 105-139-34 + tcl::dict::set TK_colour_map orange 255-165-0 + tcl::dict::set TK_colour_map "orange red" 255-69-0 + tcl::dict::set TK_colour_map orange1 255-165-0 + tcl::dict::set TK_colour_map orange2 238-154-0 + tcl::dict::set TK_colour_map orange3 205-133-0 + tcl::dict::set TK_colour_map orange4 139-90-0 + tcl::dict::set TK_colour_map OrangeRed 255-69-0 + tcl::dict::set TK_colour_map OrangeRed1 255-69-0 + tcl::dict::set TK_colour_map OrangeRed2 238-64-0 + tcl::dict::set TK_colour_map OrangeRed3 205-55-0 + tcl::dict::set TK_colour_map OrangeRed4 139-37-0 + tcl::dict::set TK_colour_map orchid 218-112-214 + tcl::dict::set TK_colour_map orchid1 255-131-250 + tcl::dict::set TK_colour_map orchid2 238-122-233 + tcl::dict::set TK_colour_map orchid3 205-105-201 + tcl::dict::set TK_colour_map orchid4 139-71-137 + tcl::dict::set TK_colour_map "pale goldenrod" 238-232-170 + tcl::dict::set TK_colour_map "pale green" 152-251-152 + tcl::dict::set TK_colour_map "pale turquoise" 175-238-238 + tcl::dict::set TK_colour_map "pale violet red" 219-112-147 + tcl::dict::set TK_colour_map PaleGoldenrod 238-232-170 + tcl::dict::set TK_colour_map PaleGreen 152-251-152 + tcl::dict::set TK_colour_map PaleGreen1 154-255-154 + tcl::dict::set TK_colour_map PaleGreen2 144-238-144 + tcl::dict::set TK_colour_map PaleGreen3 124-205-124 + tcl::dict::set TK_colour_map PaleGreen4 84-139-84 + tcl::dict::set TK_colour_map PaleTurquoise 175-238-238 + tcl::dict::set TK_colour_map PaleTurquoise1 187-255-255 + tcl::dict::set TK_colour_map PaleTurquoise2 174-238-238 + tcl::dict::set TK_colour_map PaleTurquoise3 150-205-205 + tcl::dict::set TK_colour_map PaleTurquoise4 102-139-139 + tcl::dict::set TK_colour_map PaleVioletRed 219-112-147 + tcl::dict::set TK_colour_map PaleVioletRed1 255-130-171 + tcl::dict::set TK_colour_map PaleVioletRed2 238-121-159 + tcl::dict::set TK_colour_map PaleVioletRed3 205-104-127 + tcl::dict::set TK_colour_map PaleVioletRed4 139-71-93 + tcl::dict::set TK_colour_map "papaya whip" 255-239-213 + tcl::dict::set TK_colour_map PapayaWhip 255-239-213 + tcl::dict::set TK_colour_map "peach puff" 255-218-185 + tcl::dict::set TK_colour_map PeachPuff 255-218-185 + tcl::dict::set TK_colour_map PeachPuff1 255-218-185 + tcl::dict::set TK_colour_map PeachPuff2 238-203-173 + tcl::dict::set TK_colour_map PeachPuff3 205-175-149 + tcl::dict::set TK_colour_map PeachPuff4 139-119-101 + tcl::dict::set TK_colour_map peru 205-133-63 + tcl::dict::set TK_colour_map pink 255-192-203 + tcl::dict::set TK_colour_map pink1 255-181-197 + tcl::dict::set TK_colour_map pink2 238-169-184 + tcl::dict::set TK_colour_map pink3 205-145-158 + tcl::dict::set TK_colour_map pink4 139-99-108 + tcl::dict::set TK_colour_map plum 221-160-221 + tcl::dict::set TK_colour_map plum1 255-187-255 + tcl::dict::set TK_colour_map plum2 238-174-238 + tcl::dict::set TK_colour_map plum3 205-150-205 + tcl::dict::set TK_colour_map plum4 139-102-139 + tcl::dict::set TK_colour_map "powder blue" 176-224-230 + tcl::dict::set TK_colour_map PowderBlue 176-224-230 + tcl::dict::set TK_colour_map purple 128-0-128 + tcl::dict::set TK_colour_map purple1 155-48-255 + tcl::dict::set TK_colour_map purple2 145-44-238 + tcl::dict::set TK_colour_map purple3 125-38-205 + tcl::dict::set TK_colour_map purple4 85-26-139 + tcl::dict::set TK_colour_map red 255-0-0 + tcl::dict::set TK_colour_map red1 255-0-0 + tcl::dict::set TK_colour_map red2 238-0-0 + tcl::dict::set TK_colour_map red3 205-0-0 + tcl::dict::set TK_colour_map red4 139-0-0 + tcl::dict::set TK_colour_map "rosy brown" 188-143-143 + tcl::dict::set TK_colour_map RosyBrown 188-143-143 + tcl::dict::set TK_colour_map RosyBrown1 255-193-193 + tcl::dict::set TK_colour_map RosyBrown2 238-180-180 + tcl::dict::set TK_colour_map RosyBrown3 205-155-155 + tcl::dict::set TK_colour_map RosyBrown4 139-105-105 + tcl::dict::set TK_colour_map "royal blue" 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue 65-105-225 + tcl::dict::set TK_colour_map RoyalBlue1 72-118-255 + tcl::dict::set TK_colour_map RoyalBlue2 67-110-238 + tcl::dict::set TK_colour_map RoyalBlue3 58-95-205 + tcl::dict::set TK_colour_map RoyalBlue4 39-64-139 + tcl::dict::set TK_colour_map "saddle brown" 139-69-19 + tcl::dict::set TK_colour_map SaddleBrown 139-69-19 + tcl::dict::set TK_colour_map salmon 250-128-114 + tcl::dict::set TK_colour_map salmon1 255-140-105 + tcl::dict::set TK_colour_map salmon2 238-130-98 + tcl::dict::set TK_colour_map salmon3 205-112-84 + tcl::dict::set TK_colour_map salmon4 139-76-57 + tcl::dict::set TK_colour_map "sandy brown" 244-164-96 + tcl::dict::set TK_colour_map SandyBrown 244-164-96 + tcl::dict::set TK_colour_map "sea green" 46-139-87 + tcl::dict::set TK_colour_map SeaGreen 46-139-87 + tcl::dict::set TK_colour_map SeaGreen1 84-255-159 + tcl::dict::set TK_colour_map SeaGreen2 78-238-148 + tcl::dict::set TK_colour_map SeaGreen3 67-205-128 + tcl::dict::set TK_colour_map SeaGreen4 46-139-87 + tcl::dict::set TK_colour_map seashell 255-245-238 + tcl::dict::set TK_colour_map seashell1 255-245-238 + tcl::dict::set TK_colour_map seashell2 238-229-222 + tcl::dict::set TK_colour_map seashell3 205-197-191 + tcl::dict::set TK_colour_map seashell4 139-134-130 + tcl::dict::set TK_colour_map sienna 160-82-45 + tcl::dict::set TK_colour_map sienna1 255-130-71 + tcl::dict::set TK_colour_map sienna2 238-121-66 + tcl::dict::set TK_colour_map sienna3 205-104-57 + tcl::dict::set TK_colour_map sienna4 139-71-38 + tcl::dict::set TK_colour_map silver 192-192-192 + tcl::dict::set TK_colour_map "sky blue" 135-206-235 + tcl::dict::set TK_colour_map SkyBlue 135-206-235 + tcl::dict::set TK_colour_map SkyBlue1 135-206-255 + tcl::dict::set TK_colour_map SkyBlue2 126-192-238 + tcl::dict::set TK_colour_map SkyBlue3 108-166-205 + tcl::dict::set TK_colour_map SkyBlue4 74-112-139 + tcl::dict::set TK_colour_map "slate blue" 106-90-205 + tcl::dict::set TK_colour_map "slate gray" 112-128-144 + tcl::dict::set TK_colour_map "slate grey" 112-128-144 + tcl::dict::set TK_colour_map SlateBlue 106-90-205 + tcl::dict::set TK_colour_map SlateBlue1 131-111-255 + tcl::dict::set TK_colour_map SlateBlue2 122-103-238 + tcl::dict::set TK_colour_map SlateBlue3 105-89-205 + tcl::dict::set TK_colour_map SlateBlue4 71-60-139 + tcl::dict::set TK_colour_map SlateGray 112-128-144 + tcl::dict::set TK_colour_map SlateGray1 198-226-255 + tcl::dict::set TK_colour_map SlateGray2 185-211-238 + tcl::dict::set TK_colour_map SlateGray3 159-182-205 + tcl::dict::set TK_colour_map SlateGray4 108-123-139 + tcl::dict::set TK_colour_map SlateGrey 112-128-144 + tcl::dict::set TK_colour_map snow 255-250-250 + tcl::dict::set TK_colour_map snow1 255-250-250 + tcl::dict::set TK_colour_map snow2 238-233-233 + tcl::dict::set TK_colour_map snow3 205-201-201 + tcl::dict::set TK_colour_map snow4 139-137-137 + tcl::dict::set TK_colour_map "spring green" 0-255-127 + tcl::dict::set TK_colour_map SpringGreen 0-255-127 + tcl::dict::set TK_colour_map SpringGreen1 0-255-127 + tcl::dict::set TK_colour_map SpringGreen2 0-238-118 + tcl::dict::set TK_colour_map SpringGreen3 0-205-102 + tcl::dict::set TK_colour_map SpringGreen4 0-139-69 + tcl::dict::set TK_colour_map "steel blue" 70-130-180 + tcl::dict::set TK_colour_map SteelBlue 70-130-180 + tcl::dict::set TK_colour_map SteelBlue1 99-184-255 + tcl::dict::set TK_colour_map SteelBlue2 92-172-238 + tcl::dict::set TK_colour_map SteelBlue3 79-148-205 + tcl::dict::set TK_colour_map SteelBlue4 54-100-139 + tcl::dict::set TK_colour_map tan 210-180-140 + tcl::dict::set TK_colour_map tan1 255-165-79 + tcl::dict::set TK_colour_map tan2 238-154-73 + tcl::dict::set TK_colour_map tan3 205-133-63 + tcl::dict::set TK_colour_map tan4 139-90-43 + tcl::dict::set TK_colour_map teal 0-128-128 + tcl::dict::set TK_colour_map thistle 216-191-216 + tcl::dict::set TK_colour_map thistle1 255-225-255 + tcl::dict::set TK_colour_map thistle2 238-210-238 + tcl::dict::set TK_colour_map thistle3 205-181-205 + tcl::dict::set TK_colour_map thistle4 139-123-139 + tcl::dict::set TK_colour_map tomato 255-99-71 + tcl::dict::set TK_colour_map tomato1 255-99-71 + tcl::dict::set TK_colour_map tomato2 238-92-66 + tcl::dict::set TK_colour_map tomato3 205-79-57 + tcl::dict::set TK_colour_map tomato4 139-54-38 + tcl::dict::set TK_colour_map turquoise 64-224-208 + tcl::dict::set TK_colour_map turquoise1 0-245-255 + tcl::dict::set TK_colour_map turquoise2 0-229-238 + tcl::dict::set TK_colour_map turquoise3 0-197-205 + tcl::dict::set TK_colour_map turquoise4 0-134-139 + tcl::dict::set TK_colour_map violet 238-130-238 + tcl::dict::set TK_colour_map "violet red" 208-32-144 + tcl::dict::set TK_colour_map VioletRed 208-32-144 + tcl::dict::set TK_colour_map VioletRed1 255-62-150 + tcl::dict::set TK_colour_map VioletRed2 238-58-140 + tcl::dict::set TK_colour_map VioletRed3 205-50-120 + tcl::dict::set TK_colour_map VioletRed4 139-34-82 + tcl::dict::set TK_colour_map wheat 245-222-179 + tcl::dict::set TK_colour_map wheat1 255-231-186 + tcl::dict::set TK_colour_map wheat2 238-216-174 + tcl::dict::set TK_colour_map wheat3 205-186-150 + tcl::dict::set TK_colour_map wheat4 139-126-102 + tcl::dict::set TK_colour_map white 255-255-255 + tcl::dict::set TK_colour_map "white smoke" 245-245-245 + tcl::dict::set TK_colour_map WhiteSmoke 245-245-245 + tcl::dict::set TK_colour_map yellow 255-255-0 + tcl::dict::set TK_colour_map "yellow green" 154-205-50 + tcl::dict::set TK_colour_map yellow1 255-255-0 + tcl::dict::set TK_colour_map yellow2 238-238-0 + tcl::dict::set TK_colour_map yellow3 205-205-0 + tcl::dict::set TK_colour_map yellow4 139-139-0 + tcl::dict::set TK_colour_map YellowGreen 154-205-50 + + variable TK_colour_map_lookup ;#same dict but with lower-case versions added + set TK_colour_map_lookup $TK_colour_map + dict for {key val} $TK_colour_map { + dict set TK_colour_map_lookup [tcl::string::tolower $key] $val ;#no need to test if already present - just set. + } + + variable TK_colour_map_reverse [dict create] + dict for {key val} $TK_colour_map { + dict lappend TK_colour_map_reverse $val $key + } + + #using same order as inital colour map + variable TK_colour_map_merge [dict create] + set seen_names [dict create] + dict for {key val} $TK_colour_map { + if {[dict exists $seen_names $key]} { + continue + } + set allnames [dict get $TK_colour_map_reverse $val] + set names [list] + foreach n $allnames { + if {$n ne $key} { + lappend names $n + } + } + dict set TK_colour_map_merge $key [dict create colour $val names $names] + foreach n $names { + dict set seen_names $n 1 + } + } + unset seen_names + + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# Secondary API namespace +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +tcl::namespace::eval ::punk::ansi::colourmap::lib { + tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase + tcl::namespace::path [tcl::namespace::parent] + #*** !doctools + #[subsection {Namespace ::punk::ansi::colourmap::lib}] + #[para] Secondary functions that are part of the API + #[list_begin definitions] + + #proc utility1 {p1 args} { + # #*** !doctools + # #[call lib::[fun utility1] [arg p1] [opt {?option value...?}]] + # #[para]Description of utility1 + # return 1 + #} + + + + #*** !doctools + #[list_end] [comment {--- end definitions namespace ::punk::ansi::colourmap::lib ---}] +} +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ + + + + +# ----------------------------------------------------------------------------- +# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked +# ----------------------------------------------------------------------------- +# variable PUNKARGS +# variable PUNKARGS_aliases +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + lappend ::punk::args::register::NAMESPACES ::punk::ansi::colourmap +} +# ----------------------------------------------------------------------------- + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::ansi::colourmap [tcl::namespace::eval ::punk::ansi::colourmap { + variable pkg ::punk::ansi::colourmap + variable version + set version 0.1.0 +}] +return + +#*** !doctools +#[manpage_end] + diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.8.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.tm similarity index 54% rename from src/vfs/_vfscommon.vfs/modules/punk/args-0.1.8.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.tm index e9211295..7710fa00 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.8.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.1.8 +# Application punk::args 0.2 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.8] +#[manpage_begin punkshell_module_punk::args 0 0.2] #[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 --}] @@ -268,6 +268,7 @@ tcl::namespace::eval punk::args::register { #[list_end] [comment {--- end definitions namespace punk::args::register ---}] } +tcl::namespace::eval ::punk::args {} # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Base namespace @@ -376,17 +377,28 @@ tcl::namespace::eval punk::args { %B%@id%N% ?opt val...? directive-options: -id %B%@cmd%N% ?opt val...? - directive-options: -name -help + directive-options: -name + -summary + -help %B%@leaders%N% ?opt val...? (used for leading args that come before switches/opts) - directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) + directive-options: + -min -max (min and max number of leaders) + -unnamed (allow unnamed positional leaders) + -takewhenargsmodulo (assign args to leaders based on modulo + of total number of args. If value is not supplied (or < 2) then + leaders are assigned based on whether configured opts are + encountered, and whether the min number of leaders and values + can be satisfied. In this case optional leaders are assigned if + the type of the argument can be matched.) + (also accepts options as defaults for subsequent leader definitions) %B%@opts%N% ?opt val...? directive-options: -any|-arbitrary + (also accepts options as defaults for subsequent flag definitions) %B%@values%N% ?opt val...? (used for trailing args that come after switches/opts) directive-options: -min -max -unnamed - (also accepts options as defaults for subsequent arguments) + (also accepts options as defaults for subsequent value definitions) %B%@form%N% ?opt val...? (used for commands with multiple forms) directive-options: -form -synopsis @@ -397,6 +409,8 @@ tcl::namespace::eval punk::args { -body (override autogenerated arg info for form) %B%@doc%N% ?opt val...? directive-options: -name -url + %B%@examples%N% ?opt val...? + directive-options: -help %B%@seealso%N% ?opt val...? directive-options: -name -url (for footer - unimplemented) @@ -426,17 +440,46 @@ tcl::namespace::eval punk::args { custom leading args, switches/options (names starting with -) and trailing values also take spec-options: - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. + -type + A typenamelist represents a multi-value clause where each + value must match the specified type in order. This is not + valid for flags - which can only take a single value. + + typename and entries in typenamelist can take 2 forms: + 1) basic form: elements of llength 1 such as a simple type, + or a pipe-delimited set of type-alternates. + e.g for a single typename: + -type int, -type int|char, -type int|literal(abc) + e.g for a typenamelist + -type {int double}, -type {int|char double} + 2) special form: elements of variable length + e.g for a single typename: + -type {{literal |}} + -type {{literal | | literal (}} + e.g for a typenamelist + -type {{literal |} {stringstartswith abc | int}} + The 2 forms can be mixed: + -type {{literal |} {stringstartswith a|c | int} literal(xyz)|int} + + Defaults to string. If no other restrictions + are required, choosing -type any does the least validation. recognised types: - int|integer + any + (unvalidated - accepts anything) + none + (used for flags/switches only. Indicates this is + a 'solo' flag ie accepts no value) + Not valid as a member of a clause's typenamelist. + int + integer number list indexexpression dict double - bool|boolean + float + bool + boolean char file directory @@ -449,13 +492,41 @@ tcl::namespace::eval punk::args { string (also any of the 'string is' types such as xdigit, graph, punct, lower etc) - any - (unvalidated - accepts anything) - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) + -type string on its own does not need validation, + but still checks for string-related restrictions + such as regexprefail, & minsize + + literal() + (exact match for string) + literalprefix() + (prefix match for string, other literal and literalprefix + entries specified as alternates using | are used in the + calculation) + stringstartswith() + (value must match glob *) + The value of string must not contain pipe char '|' + + Note that types can be combined with | to indicate an 'or' + operation + e.g char|int + e.g literal(xxx)|literal(yyy) + e.g literalprefix(text)|literalprefix(binary) + (when all in the pipe-delimited type-alternates set are + literal or literalprefix - this is similar to the -choices + option) + and more.. (todo - document here) + If a typenamelist is supplied and has length > 1 + then -typeranges must be used instead of -range + The number of elements in -typeranges must match + the number of elements specified in -type. + + -typesynopsis + Must be same length as value in -type + This provides and override for synopsis display of types. + Any desired italicization must be applied manually to the + value. -optional (defaults to true for flags/switches false otherwise) @@ -534,7 +605,8 @@ tcl::namespace::eval punk::args { entries in -choices/-choicegroups. -minsize (type dependant) -maxsize (type dependant) - -range (type dependant) + -range (type dependant - only valid if -type is a single item) + -typeranges (list with same number of elements as -type) " @@ -550,7 +622,8 @@ tcl::namespace::eval punk::args { inner loops in more performance-sensitive code. " @values -min 1 -max -1 - text -type string -multiple 1 -help\ + #text should be a well-formed Tcl list + text -type list -multiple 1 -help\ {Block(s) of text representing the argument definition for a command. At least one must be supplied. If multiple, they are joined together with \n. Using multiple text arguments may be useful to mix curly-braced and double-quoted @@ -590,8 +663,8 @@ tcl::namespace::eval punk::args { proc New_command_form {name} { #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ + set leaderdirective_defaults [tcl::dict::create\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -605,8 +678,8 @@ tcl::namespace::eval punk::args { -validationtransform {}\ -ensembleparameter 0\ ] - set optspec_defaults [tcl::dict::create\ - -type string\ + set optdirective_defaults [tcl::dict::create\ + -type any\ -optional 1\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -619,9 +692,13 @@ tcl::namespace::eval punk::args { -regexprepass {}\ -validationtransform {}\ -prefix 1\ + -parsekey ""\ + -group ""\ ] - set valspec_defaults [tcl::dict::create\ - -type string\ + #parsekey is name of argument to use as a key in punk::args::parse result dicts + + set valdirective_defaults [tcl::dict::create\ + -type any\ -optional 0\ -allow_ansi 1\ -validate_ansistripped 0\ @@ -646,8 +723,9 @@ tcl::namespace::eval punk::args { LEADER_NAMES [list]\ LEADER_MIN ""\ LEADER_MAX ""\ + LEADER_TAKEWHENARGSMODULO 0\ LEADER_UNNAMED false\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ + LEADERSPEC_DEFAULTS $leaderdirective_defaults\ LEADER_CHECKS_DEFAULTS {}\ OPT_DEFAULTS [tcl::dict::create]\ OPT_REQUIRED [list]\ @@ -656,15 +734,16 @@ tcl::namespace::eval punk::args { OPT_MIN ""\ OPT_MAX ""\ OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ + OPTSPEC_DEFAULTS $optdirective_defaults\ OPT_CHECKS_DEFAULTS {}\ + OPT_GROUPS {}\ VAL_DEFAULTS [tcl::dict::create]\ VAL_REQUIRED [list]\ VAL_NAMES [list]\ VAL_MIN ""\ VAL_MAX ""\ VAL_UNNAMED false\ - VALSPEC_DEFAULTS $valspec_defaults\ + VALSPEC_DEFAULTS $valdirective_defaults\ VAL_CHECKS_DEFAULTS {}\ FORMDISPLAY [tcl::dict::create]\ ] @@ -983,6 +1062,7 @@ tcl::namespace::eval punk::args { #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table set seealso_info {} set keywords_info {} + set examples_info {} ###set leader_min 0 ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit #set leader_max "" @@ -1011,7 +1091,7 @@ tcl::namespace::eval punk::args { set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict if {[llength $record_values] % 2 != 0} { #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" + error "punk::args::resolve - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" } # ---------------------------------------------------------- # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. @@ -1097,7 +1177,7 @@ tcl::namespace::eval punk::args { if {[dict exists $at_specs -id]} { set thisid [dict get $at_specs -id] if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" + error "punk::args::resolve @id mismatch existing: $id vs $thisid" } } set id_info $at_specs @@ -1114,6 +1194,8 @@ tcl::namespace::eval punk::args { } } default { + #NOTE - this is switch arm for the literal "default" (@default) - not the default arm of the switch block! + #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) @@ -1155,7 +1237,29 @@ tcl::namespace::eval punk::args { # arity system ? #handle multiple parsing styles based on arities and keyword positions (and/or flags?) #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each + + # @form "-synopsis" is optional - and only exists in case the user really wants + # to display something different. The system should generate consistent synopses + # with appropriate italics/bracketing etc. + # For manual -synopsis - features such as italics must be manually added. + + #spitballing.. + #The punk::args parser should generally be able to determine the appropriate form based + #on supplied arguments, e.g automatically using argument counts and matching literals. + #We may need to support some hints for forcing more efficient -form discriminators + # + # e.g compare with -takewhenargsmodulo that is available on @leaders + + #the -arities idea below is a rough one; potentially something to consider.. but + #we want to be able to support command completion.. and things like literals should probably + #take preference for partially typed commands.. as flipping to other forms based on argcount + #could be confusing. Need to match partial command to closest form automatically but allow + #user to lock in a form interactively and see mismatches (?) + #Probably the arity-ranges of a form are best calculated automatically rather than explicitly, + #otherwise we have a strong potential for misdefinition.. (conflict with defined leaders,opts,values) + #The way forward might be to calculate some 'arity' structure from the forms to aid in form-discrimination at arg parse time. + #(this is currently covered in some ways by the LEADER_MIN,LEADER_MAX,OPT_MIN,OPT_MAX,VAL_MIN,VAL_MAX members of the FORMS dict.) + # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ # -arities { # 2 @@ -1183,7 +1287,6 @@ tcl::namespace::eval punk::args { # } #todo - #can we generate a form synopsis if -synopsis not supplied? #form id can be list of ints|names?, or * if {[dict exists $at_specs -form]} { @@ -1201,6 +1304,9 @@ tcl::namespace::eval punk::args { } cmd { #allow arbitrary - review + #e.g -name + # -summary + # -help set cmd_info [dict merge $cmd_info $at_specs] } doc { @@ -1217,7 +1323,7 @@ tcl::namespace::eval punk::args { opts { foreach fid $record_form_ids { if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" + error "punk::args::resolve - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" } tcl::dict::set F $fid argspace "options" set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] @@ -1239,7 +1345,7 @@ tcl::namespace::eval punk::args { #if no -max explicitly specified, and llength OPT_NAMES == 0 and OPT_ANY == 0 - -max will be set to 0 below. dict set F $fid OPT_MAX $v } - -minsize - -maxsize - -range - + -minsize - -maxsize - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted { #review - only apply to certain types? @@ -1252,30 +1358,65 @@ tcl::namespace::eval punk::args { } } -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { + #v is a typelist + #foreach t $v { + # #validate? + #} + tcl::dict::set tmp_optspec_defaults -type $v + } + -parsekey { + tcl::dict::set tmp_optspec_defaults -parsekey $v - } - default { - #todo - disallow unknown types unless prefixed with custom- + } + -group { + tcl::dict::set tmp_optspec_defaults -group $v + if {$v ne "" && ![tcl::dict::exists $F $fid OPT_GROUPS $v]} { + tcl::dict::set F $fid OPT_GROUPS $v {-parsekey {} -help {}} + } + if {$v ne ""} { + if {[tcl::dict::exists $at_specs -parsekey]} { + tcl::dict::set F $fid OPT_GROUPS $v -parsekey [tcl::dict::get $at_specs -parsekey] } } - tcl::dict::set tmp_optspec_defaults -type $v + } + -grouphelp { + if {![tcl::dict::exists $at_specs -group]} { + error "punk::args::resolve Bad @opt line. -group entry is required if -grouphelp is being configured. @id:$DEF_definition_id" + } + set g [tcl::dict::get $at_specs -group] + if {$g eq ""} { + error "punk::args::resolve Bad @opt line. -group non-empty value is required if -grouphelp is being configured. @id:$DEF_definition_id" + } + set groupdict [tcl::dict::get $F $fid OPT_GROUPS] + #set helprecords [tcl::dict::get $F $fid OPT_GROUPS_HELP] + if {![tcl::dict::exists $groupdict $g]} { + tcl::dict::set F $fid OPT_GROUPS $g [dict create -parsekey {} -help $v] + } else { + tcl::dict::set F $fid OPT_GROUPS $g -help $v + } + } + -range { + if {[dict exists $at_specs -type]} { + set tp [dict get $at_specs -type] + } else { + set tp [dict get $tmp_optspec_defaults -type] + } + if {[llength $tp] == 1} { + tcl::dict::set tmp_optspec_defaults -typeranges [list $v] + } else { + error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" + } + } + -typeranges { + if {[dict exists $at_specs -type]} { + set tp [dict get $at_specs -type] + } else { + set tp [dict get $tmp_optspec_defaults -type] + } + if {[llength $tp] != [llength $v]} { + error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -typeranges has length [llength $v]. Lengths must match. @id:$DEF_definition_id" + } + tcl::dict::set tmp_optspec_defaults -typeranges $v } -regexprepass - -regexprefail - @@ -1293,18 +1434,20 @@ tcl::namespace::eval punk::args { -prefix { #check is bool if {![string is boolean -strict $v]} { - error "punk::args::define - Option '$k' has value '$v'of wrong type in @opts line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - Option '$k' has value '$v'of wrong type in @opts line. Must be boolean @id:$DEF_definition_id" } tcl::dict::set tmp_optspec_defaults $k $v } default { - set known { -any -anyopts -arbitrary -form -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ + set known { -parsekey -group -grouphelp\ + -any -anyopts -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ + -type -range -typeranges -default -typedefaults -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase\ -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ -regexprepass -regexprefail -regexprefailmsg -validationtransform\ } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" + error "punk::args::resolve - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" } } } @@ -1314,7 +1457,7 @@ tcl::namespace::eval punk::args { leaders { foreach fid $record_form_ids { if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" + error "punk::args::resolve - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" } set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] @@ -1326,7 +1469,7 @@ tcl::namespace::eval punk::args { -min - -minvalues { if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" } dict set F $fid LEADER_MIN $v #if {$leader_max == 0} { @@ -1336,25 +1479,23 @@ tcl::namespace::eval punk::args { -max - -maxvalues { if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" } dict set F $fid LEADER_MAX $v } + -takewhenargsmodulo { + dict set F $fid LEADER_TAKEWHENARGSMODULO $v + } -choiceprefix - -choicerestricted { if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" } tcl::dict::set tmp_leaderspec_defaults $k $v } - -minsize - -maxsize - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefixdenylist - -choiceprefixreservelist - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } -choiceinfo - -choicelabels { if {[llength $v] %2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" + error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" } tcl::dict::set tmp_leaderspec_defaults $k $v } @@ -1366,29 +1507,44 @@ tcl::namespace::eval punk::args { } } -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } + #$v is a list of types + #foreach t $v { + #validate? + #} + #switch -- $v { + # int - integer { + # set v int + # } + # char - character { + # set v char + # } + # bool - boolean { + # set v bool + # } + # dict - dictionary { + # set v dict + # } + # list { + + # } + # index { + # set v indexexpression + # } + # default { + # #todo - disallow unknown types unless prefixed with custom- + # } + #} + tcl::dict::set tmp_leaderspec_defaults $k $v + } + -range { + tcl::dict::set tmp_leaderspec_defaults -range $v + } + -typeranges { + tcl::dict::set tmp_leaderspec_defaults -range $v + } + -minsize - -maxsize - + -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefixdenylist - -choiceprefixreservelist - -nocase { + #review - only apply to certain types? tcl::dict::set tmp_leaderspec_defaults $k $v } -regexprepass - @@ -1403,20 +1559,20 @@ tcl::namespace::eval punk::args { -strip_ansi - -multiple { if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" } tcl::dict::set tmp_leaderspec_defaults $k $v } -unnamed { if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - invalid type of value '$v' for key '$k' in @leaders line. Must be boolean @id:$DEF_definition_id" } dict set F $fid LEADER_UNNAMED $v } -ensembleparameter { #review tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" + #error "punk::args::resolve - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" } default { set known { -min -form -minvalues -max -maxvalues\ @@ -1428,7 +1584,7 @@ tcl::namespace::eval punk::args { -regexprepass -regexprefail -regexprefailmsg -validationtransform\ -unnamed\ } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" } } } @@ -1451,7 +1607,7 @@ tcl::namespace::eval punk::args { -min - -minvalues { if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is 0. got $v @id:$DEF_definition_id" } #set val_min $v dict set F $fid VAL_MIN $v @@ -1459,19 +1615,19 @@ tcl::namespace::eval punk::args { -max - -maxvalues { if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" + error "punk::args::resolve - minimum acceptable value for key '$k' in @values line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" } #set val_max $v dict set F $fid VAL_MAX $v } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - + -minsize - -maxsize - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -nocase { #review - only apply to certain types? tcl::dict::set tmp_valspec_defaults $k $v } -choiceinfo - -choicegroups { if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" + error "punk::args::resolve - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" } tcl::dict::set tmp_valspec_defaults $k $v } @@ -1508,13 +1664,19 @@ tcl::namespace::eval punk::args { } tcl::dict::set tmp_valspec_defaults $k $v } + -range { + tcl::dict::set tmp_valspec_defaults -range $v + } + -typeranges { + tcl::dict::set tmp_valspec_defaults -typeranges $v + } -optional - -allow_ansi - -validate_ansistripped - -strip_ansi - -multiple { if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" } tcl::dict::set tmp_valspec_defaults $k $v } @@ -1526,22 +1688,23 @@ tcl::namespace::eval punk::args { } -unnamed { if {![string is boolean -strict $v]} { - error "punk::args::define - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" + error "punk::args::resolve - invalid type of value '$v' for key '$k' in @values line. Must be boolean @id:$DEF_definition_id" } dict set F $fid VAL_UNNAMED $v } default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ + set known { -type -range -typeranges\ + -min -form -minvalues -max -maxvalues\ + -minsize -maxsize\ -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo\ -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ -nocase\ -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ + -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ -regexprepass -regexprefail -regexprefailmsg -validationtransform\ -unnamed\ } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" } } } @@ -1558,8 +1721,11 @@ tcl::namespace::eval punk::args { #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? set keywords_info [dict merge $keywords_info $at_specs] } + examples { + set examples_info [dict merge $examples_info $at_specs] + } default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @examples @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" } } #record_type directive @@ -1644,30 +1810,39 @@ tcl::namespace::eval punk::args { #e.g -myflag -type {list int} # e.g called on commandline with cmd -myflag {a b c} 3 #review - seems an unlikely and complicating feature to allow - evidence of tools using/supporting this in the wild not known of. - error "punk::args::define - Multiple space-separated arguments (as indicated by -type having multiple entries) for a flag are not supported. flag $argname -type '$tp' @id:$DEF_definition_id" + error "punk::args::resolve - Multiple space-separated arguments (as indicated by -type having multiple entries) for a flag are not supported. flag $argname -type '$tp' @id:$DEF_definition_id" + } + if {$argname eq "--"} { + if {$tp ne "none"} { + #error to explicitly attempt to configure -- as a value-taking option + error "punk::args::resolve - special flag named -- cannot be configured as a value-accepting flag. set -type none or omit -type from definition. @id:$DEF_definition_id" + } } if {$tp eq "none"} { if {$has_equal} { - error "punk::args::define - flag type 'none' (indicating non-parameter-taking flag) is not supported when any flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" + error "punk::args::resolve - flag type 'none' (indicating non-parameter-taking flag) is not supported when any flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" } } elseif {[string match {\?*\?} $tp]} { #optional flag value if {!$has_equal} { - error "punk::args::define - Optional flag parameter (as indicated by leading & trailing ?) is not supported when no flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" + error "punk::args::resolve - Optional flag parameter (as indicated by leading & trailing ?) is not supported when no flag member ends with = (indicating gnu-longopt style possibly taking a parameter). flag $argname -type '$tp' @id:$DEF_definition_id" } } } # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + ##set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] tcl::dict::set argdef_values -ARGTYPE option + #set all_choices [_resolve_get_record_choices] foreach fid $record_form_ids { if {[dict get $F $fid argspace] eq "leaders"} { dict set F $fid argspace "options" } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" + error "punk::args::resolve - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" } set record_type option + dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] } @@ -1691,7 +1866,7 @@ tcl::namespace::eval punk::args { tcl::dict::set F $fid LEADER_NAMES $temp_leadernames } else { #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" + error "punk::args::resolve - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" } if {[dict get $F $fid LEADER_MAX] >= 0} { @@ -1708,7 +1883,7 @@ tcl::namespace::eval punk::args { lappend temp_valnames $argname tcl::dict::set F $fid VAL_NAMES $temp_valnames } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" + error "punk::args::resolve - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" } #lappend val_names $argname if {[dict get $F $fid VAL_MAX] >= 0} { @@ -1724,7 +1899,7 @@ tcl::namespace::eval punk::args { } - #assert - we only get here if it is a value or flag specification line. + #assert - we only get here if it is a leader, value or flag specification line. #assert argdef_values has been set to the value of record_values foreach fid $record_form_ids { @@ -1742,102 +1917,102 @@ tcl::namespace::eval punk::args { foreach {spec specval} $argdef_values { #literal-key switch - bytecompiled to jumpTable switch -- $spec { - -form { - - } + -form {} -type { #todo - could be a list e.g {any int literal(Test)} #case must be preserved in literal bracketed part set typelist [list] foreach typespec $specval { - set lc_typespec [tcl::string::tolower $typespec] - if {[string match {\?*\?} $lc_typespec]} { - set lc_type [string range $lc_typespec 1 end-1] + if {[string match {\?*\?} $typespec]} { + set tspec [string range $typespec 1 end-1] set optional_clausemember true } else { - set lc_type $lc_typespec + set tspec $typespec set optional_clausemember false } - #normalize here so we don't have to test during actual args parsing in main function - set normtype "" ;#assert - should be overridden in all branches of switch - switch -- $lc_type { - int - integer { - set normtype int - } - double - float { - #review - user may wish to preserve 'float' in help display - consider how best to implement - set normtype double - } - bool - boolean { - set normtype bool - } - char - character { - set normtype char - } - dict - dictionary { - set normtype dict - } - index - indexexpression { - set normtype indexexpression - } - "" - none - solo { - if {$is_opt} { - #review - are we allowing clauses for flags? - #e.g {-flag -type {int int}} - #this isn't very tcl like, where we'd normally mark the flag with -multiple true and - # instead require calling as: -flag -flag - #It seems this is a reasonably rare/unlikely requirement in most commandline tools. - - if {[llength $specval] > 1} { - #makes no sense to have 'none' in a clause - error "punk::args::define - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" - } - #tcl::dict::set spec_merged -type none - set normtype none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + set type_alternatives [_split_type_expression $tspec] + set normlist [list] + foreach alt $type_alternatives { + set firstword [lindex $alt 0] + set lc_firstword [tcl::string::tolower $firstword] + #normalize here so we don't have to test during actual args parsing in main function + set normtype "" ;#assert - should be overridden in all branches of switch + switch -- $lc_firstword { + int - integer {set normtype int} + double - float { + #review - user may wish to preserve 'float' in help display - consider how best to implement + set normtype double + } + bool - boolean {set normtype bool} + char - character {set normtype char} + dict - dictionary {set normtype dict} + index - indexexpression {set normtype indexexpression} + "" - none - solo { + if {$is_opt} { + #review - are we allowing clauses for flags? + #e.g {-flag -type {int int}} + #this isn't very tcl like, where we'd normally mark the flag with -multiple true and + # instead require calling as: -flag -flag + #It seems this is a reasonably rare/unlikely requirement in most commandline tools. + + if {[llength $specval] > 1} { + #makes no sense to have 'none' in a clause + error "punk::args::resolve - invalid -type '$specval' for flag '$argname' ('none' in multitype) @id:$DEF_definition_id" + } + #tcl::dict::set spec_merged -type none + set normtype none + if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { + tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. + } + } else { + #solo only valid for flags + error "punk::args::resolve - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } else { - #solo only valid for flags - error "punk::args::define - invalid -type 'none|solo' for positional argument '$argname' (only valid for flags/options) @id:$DEF_definition_id" } - } - any - anything { - set normtype any - } - ansi - ansistring { - set normtype ansistring - } - string - globstring { - set normtype $lc_type - } - literal { - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" + any - anything {set normtype any} + ansi - ansistring {set normtype ansistring} + string - globstring {set normtype $lc_firstword} + literal { + #value was split out by _split_type_expression + set normtype literal([lindex $alt 1]) } - #value is the name of the argument - set normtype literal - } - default { - if {[string match literal* $lc_type]} { - #typespec may or may not be of form ?xxx? - set literal_tail [string range [string trim $typespec ?] 7 end] - set normtype literal$literal_tail - } else { + literalprefix { + set normtype literalprefix([lindex $alt 1]) + } + stringstartswith { + set normtype stringstartswith([lindex $alt 1]) + } + stringendswith { + set normtype stringendswith([lindex $alt 1]) + } + default { #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW #tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - set normtype $lc_type + #todo + set normtype $alt } } + lappend normlist $normtype } + set norms [join $normlist |] if {$optional_clausemember} { - lappend typelist ?$normtype? + lappend typelist ?$norms? } else { - lappend typelist $normtype + lappend typelist $norms } } tcl::dict::set spec_merged -type $typelist } + -typesynopsis { + set typecount [llength [tcl::dict::get $spec_merged -type]] + if {$typecount != [llength $specval]} { + error "punk::args::resolve - invalid -typesynopsis specification for argument '$argname'. -typesynopsis has [llength $specval] entries, but requires $typecount entries (one for each entry in -types. Use empty string list members for default) @id:$DEF_definition_id" + } + tcl::dict::set spec_merged -typesynopsis $specval + } + -parsekey - -group { + tcl::dict::set spec_merged -typesynopsis $specval + } -solo - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choiceprefixreservelist - -choicerestricted - -choicelabels - -choiceinfo - @@ -1852,31 +2027,38 @@ tcl::namespace::eval punk::args { -range { #allow simple case to be specified without additional list wrapping #only multi-types require full list specification - #-flag1 -type int -range {0 4} - #-flag2 -type {int string} -range {{0 4} {"" ""}} + #arg1 -type int -range {0 4} + #arg2 -type {int string} -range {{0 4} {"" ""}} set typecount [llength [tcl::dict::get $spec_merged -type]] if {$typecount == 1} { - tcl::dict::set spec_merged $spec [list $specval] + tcl::dict::set spec_merged -typeranges [list $specval] } else { - if {$typecount != [llength $specval]} { - error "punk::args::define - invalid -range specifications for argument '$argname' requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" - } - tcl::dict::set spec_merged $spec $specval + error "punk::args::resolve Bad @opt line. -type has length [llength $tp] (-type $tp). -range only applies to single-item type. Use -typeranges instead. @id:$DEF_definition_id" } } - -default { - #JJJ + -typeranges { set typecount [llength [tcl::dict::get $spec_merged -type]] - if {$typecount == 1} { - #tcl::dict::set spec_merged -default [list $specval] - tcl::dict::set spec_merged -default $specval - } else { - tcl::dict::set spec_merged -default $specval + if {$typecount != [llength $specval]} { + error "punk::args::resolve - invalid -typeranges specification for argument '$argname'. -typeranges has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" } + tcl::dict::set spec_merged -typeranges $specval + } + -default { + #The -default is for when the entire clause is missing + #It doesn't necessarily have to have the same number of elements as the clause {llength $typelist} + #review + tcl::dict::set spec_merged -default $specval if {![dict exists $argdef_values -optional]} { tcl::dict::set spec_merged -optional 1 } } + -typedefaults { + set typecount [llength [tcl::dict::get $spec_merged -type]] + if {$typecount != [llength $specval]} { + error "punk::args::resolve - invalid -typedefaults specification for argument '$argname'. -typedefaults has [llength $specval] entries, but requires $typecount entries (one for each entry in -types) @id:$DEF_definition_id" + } + tcl::dict::set spec_merged -typedefaults $specval + } -optional { #applies to whole arg - not each -type tcl::dict::set spec_merged -optional $specval @@ -1894,18 +2076,19 @@ tcl::namespace::eval punk::args { -validationtransform { #string is dict only 8.7/9+ if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" + error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" } dict for {tk tv} $specval { switch -- $tk { - -function - -type - -minsize - -maxsize - -range { + -command - -function - -type - -minsize - -maxsize - -range { } default { set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" + error "punk::args::resolve - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" } } } + #TODO! } default { @@ -1913,7 +2096,7 @@ tcl::namespace::eval punk::args { #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" + puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" } else { set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" if {$targetswitch eq "-*"} { @@ -1922,18 +2105,23 @@ tcl::namespace::eval punk::args { if {[tcl::dict::exists $refs $specval $targetswitch]} { tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" + puts stderr "punk::args::resolve argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" } } } } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups\ + set known_argopts [list\ + -form -type\ + -parsekey -group\ + -range -typeranges\ + -default -typedefaults\ + -minsize -maxsize -choices -choicegroups\ -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted\ -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ -regexprepass -regexprefail -regexprefailmsg -validationtransform\ -ensembleparameter\ ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" + error "punk::args::resolve - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" } } } @@ -1942,14 +2130,17 @@ tcl::namespace::eval punk::args { if {$is_opt} { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize + if {$argname eq "--"} { + #force -type none - in case no -type was specified and @opts -type is some other default such as string + tcl::dict::set spec_merged -type none + } if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] } } else { tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize + [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize } tcl::dict::set F $fid ARG_INFO $argname $spec_merged #review existence of -default overriding -optional @@ -1975,13 +2166,16 @@ tcl::namespace::eval punk::args { if {[tcl::dict::exists $spec_merged -default]} { if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] + #JJJ + set parsekey [dict get $F $fid ARG_INFO $argname -default] + if {$parsekey eq ""} { + set parsekey $argname + } tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } else { if {[dict get $F $fid argspace] eq "leaders"} { tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] } } @@ -2083,6 +2277,8 @@ tcl::namespace::eval punk::args { doc_info $doc_info\ package_info $package_info\ seealso_info $seealso_info\ + keywords_info $keywords_info\ + examples_info $examples_info\ id_info $id_info\ FORMS $F\ form_names [dict keys $F]\ @@ -2113,9 +2309,9 @@ tcl::namespace::eval punk::args { namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} + variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @examples @formdisplay @seealso @leaders @opts @values leaders opts values} variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} + directives {@id @package @cmd @ref @doc @examples @formdisplay @seealso} argumenttypes {leaders opts values} remaining_defaults {@leaders @opts @values} } @@ -2322,7 +2518,7 @@ tcl::namespace::eval punk::args { dict set resultdict @id [list -id [dict get $specdict id]] } } - foreach directive {@package @cmd @doc @seealso} { + foreach directive {@package @cmd @doc @examples @seealso} { set dshort [string range $directive 1 end] if {"$directive" in $included_directives} { if {[dict exists $opt_override $directive]} { @@ -2336,6 +2532,7 @@ tcl::namespace::eval punk::args { } #todo @formdisplay + #todo @ref ? #output ordered by leader, option, value @@ -2387,7 +2584,7 @@ tcl::namespace::eval punk::args { } } } - @package - @cmd - @doc - @seealso { + @package - @cmd - @doc - @examples - @seealso { if {"$type" in $included_directives} { set tp [string range $type 1 end] ;# @package -> package if {[dict exists $opt_override $type]} { @@ -2569,6 +2766,10 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + proc aliases {} { + variable aliases + punk::lib::showdict $aliases + } proc set_alias {alias id} { variable aliases dict set aliases $alias $id @@ -2819,7 +3020,8 @@ tcl::namespace::eval punk::args { set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] #puts "-->$cmdinfo" #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { + set maxloop 10 ;#failsafe + while {[string last \n $cmdinfo] >= 1 && $maxloop > -1} { #looks like a script - haven't gone up far enough? #(e.g patternpunk oo system: >punk . poses -invalidoption) incr call_level -1 @@ -2841,6 +3043,7 @@ tcl::namespace::eval punk::args { break } } + incr maxloop -1 } set caller [regexp -inline {\S+} $cmdinfo] if {$caller eq "namespace"} { @@ -2929,62 +3132,81 @@ tcl::namespace::eval punk::args { "Ordinal index or name of command form" }] ] - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } + variable arg_error_CLR array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] variable arg_error_CLR_nocolour array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" variable arg_error_CLR_info array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] variable arg_error_CLR_error array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] + + proc _argerror_load_colours {{forcereload 0}} { + variable arg_error_CLR + #todo - option for reload/retry? + if {!$forcereload && [array size arg_error_CLR] > 0} { + return + } + + if {[catch {package require punk::ansi} errMsg]} { + puts stderr "punk::args FAILED to load punk::ansi\n$errMsg" + proc ::punk::args::a {args} {} + proc ::punk::args::a+ {args} {} + } else { + namespace eval ::punk::args { + namespace import ::punk::ansi::a ::punk::ansi::a+ + } + } + #array set arg_error_CLR {} + set arg_error_CLR(testsinglecolour) [a+ yellow] ;#A single SGR colour to test current colour on|off state (empty string vs some result - used to determine if forcereload required) + set arg_error_CLR(errormsg) [a+ brightred] + set arg_error_CLR(title) "" + set arg_error_CLR(check) [a+ brightgreen] + set arg_error_CLR(solo) [a+ brightcyan] + set arg_error_CLR(choiceprefix) [a+ underline] + set arg_error_CLR(badarg) [a+ brightred] + set arg_error_CLR(goodarg) [a+ green strike] + set arg_error_CLR(goodchoice) [a+ reverse] + set arg_error_CLR(linebase_header) [a+ white] + set arg_error_CLR(cmdname) [a+ brightwhite] + set arg_error_CLR(groupname) [a+ bold] + set arg_error_CLR(ansiborder) [a+ bold] + set arg_error_CLR(ansibase_header) [a+ bold] + set arg_error_CLR(ansibase_body) [a+ white] + variable arg_error_CLR_nocolour + #array set arg_error_CLR_nocolour {} + set arg_error_CLR_nocolour(errormsg) [a+ bold] + set arg_error_CLR_nocolour(title) [a+ bold] + set arg_error_CLR_nocolour(check) "" + set arg_error_CLR_nocolour(solo) "" + set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment + set arg_error_CLR_nocolour(goodarg) [a+ strike] + set arg_error_CLR_nocolour(cmdname) [a+ bold] + set arg_error_CLR_nocolour(linebase_header) "" + set arg_error_CLR_nocolour(linebase) "" + set arg_error_CLR_nocolour(ansibase_body) "" + variable arg_error_CLR_info + #array set arg_error_CLR_info {} + set arg_error_CLR_info(errormsg) [a+ brightred bold] + set arg_error_CLR_info(title) [a+ brightyellow bold] + set arg_error_CLR_info(check) [a+ brightgreen bold] + set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_info(groupname) [a+ cyan bold] + set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] + set arg_error_CLR_info(ansibase_header) [a+ cyan] + set arg_error_CLR_info(ansibase_body) [a+ white] + variable arg_error_CLR_error + #array set arg_error_CLR_error {} + set arg_error_CLR_error(errormsg) [a+ brightred bold] + set arg_error_CLR_error(title) [a+ brightcyan bold] + set arg_error_CLR_error(check) [a+ brightgreen bold] + set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] + set arg_error_CLR_error(groupname) [a+ cyan bold] + set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] + set arg_error_CLR_error(ansibase_header) [a+ yellow] + set arg_error_CLR_error(ansibase_body) [a+ white] + } #bas ic recursion blocker @@ -3026,7 +3248,21 @@ tcl::namespace::eval punk::args { error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" } + #set arg_error_CLR(testsinglecolour) [a+ brightred] + upvar ::punk::args::arg_error_CLR 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)]} { + set terminal_colour_is_on [expr {[string length [a+ yellow]]}] + set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + if {$terminal_colour_is_on ^ $error_colour_is_on} { + #results differ + set forcereload 1 + } + } + _argerror_load_colours $forcereload + if {[llength $args] %2 != 0} { + set arg_error_isrunning 0 error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" } @@ -3035,7 +3271,12 @@ 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] + #----------------------- + #todo!! make changeable from config file + #JJJ 2025-07-16 set returntype table ;#table as string + #set returntype string + #---------------------- set as_error 1 ;#usual case is to raise an error set scheme error set form 0 @@ -3119,12 +3360,11 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR switch -- $scheme { nocolour { variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour + array set CLR [array get arg_error_CLR_nocolour] } info { variable arg_error_CLR_info @@ -3171,11 +3411,12 @@ tcl::namespace::eval punk::args { append errmsg \n } } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] + set cmdname [Dict_getdef $spec_dict cmd_info -name ""] + set cmdsummary [Dict_getdef $spec_dict cmd_info -summary ""] + set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] + set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] + set docurl [Dict_getdef $spec_dict doc_info -url ""] #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] @@ -3212,16 +3453,19 @@ tcl::namespace::eval punk::args { set docurl_display "" } #synopsis - set synopsis "" + set synopsis "# [Dict_getdef $spec_dict cmd_info -summary {}]\n" set form_info [dict get $spec_dict form_info] dict for {fid finfo} $form_info { set form_synopsis [Dict_getdef $finfo -synopsis ""] if {$form_synopsis eq ""} { #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] + set form_synopsis [punk::args::synopsis -noheader -form $fid [dict get $spec_dict id]] + set ansifree_synopsis [punk::ansi::ansistripraw $form_synopsis] + if {[string length $ansifree_synopsis] > 90} { + # + set form_synopsis [punk::args::synopsis -noheader -return summary -form $fid [dict get $spec_dict id]] } + #review if {[string match (autodef)* $form_synopsis]} { set form_synopsis [string range $form_synopsis 9 end] } @@ -3349,17 +3593,18 @@ tcl::namespace::eval punk::args { set opt_names [list] set opt_names_display [list] + set opt_names_hints [list] ;#comments in first column below name display. set lookup_optset [dict create] if {[llength [dict get $form_dict OPT_NAMES]]} { set all_opts [list] - foreach optset [dict get $form_dict OPT_NAMES] { + foreach optionset [dict get $form_dict OPT_NAMES] { #e.g1 "-alias1|-realname" #e.g2 "-f|--filename" (fossil longopt style) #e.g3 "-f|--filename=" (gnu longopt style) - set optmembers [split $optset |] + set optmembers [split $optionset |] lappend all_opts {*}$optmembers foreach o $optmembers { - dict set lookup_optset $o $optset + dict set lookup_optset $o $optionset #goodargs } } @@ -3385,6 +3630,11 @@ tcl::namespace::eval punk::args { $trie destroy foreach optset [dict get $form_dict OPT_NAMES] { set arginfo [dict get $form_dict ARG_INFO $optset] + set parsekey [dict get $arginfo -parsekey] + set storageinfo "" + if {$parsekey ne "" && $parsekey ne $optset} { + set storageinfo "(stored as: $parsekey)" + } if {[dict get $arginfo -prefix]} { set opt_members [split $optset |] set odisplay [list] @@ -3396,8 +3646,7 @@ tcl::namespace::eval punk::args { set tail "" } else { set idlen [string length $id] - set prefix [string range $opt 0 $idlen-1] - set tail [string range $opt $idlen end] + lassign [punk::lib::string_splitbefore $opt $idlen] prefix tail } lappend odisplay $A_PREFIX$prefix$A_PREFIXEND$tail } @@ -3406,12 +3655,23 @@ tcl::namespace::eval punk::args { } else { lappend opt_names_display $optset } + lappend opt_names_hints $storageinfo #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] lappend opt_names $optset } } else { set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names + foreach optset [dict get $form_dict OPT_NAMES] { + set arginfo [dict get $form_dict ARG_INFO $optset] + set parsekey [dict get $arginfo -parsekey] + set storageinfo "" + if {$parsekey ne "" && $parsekey ne $optset} { + set storageinfo "(stored as: $parsekey)" + } + lappend opt_names_display $optset + lappend opt_names_hints $storageinfo + } + #set opt_names_display $opt_names } } set leading_val_names [dict get $form_dict LEADER_NAMES] @@ -3430,18 +3690,84 @@ tcl::namespace::eval punk::args { # set leading_val_names {} #} set leading_val_names_display $leading_val_names + 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 $parsed_leaders] [list opts $opt_names_display $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { + 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 + set lastgroup "" + set lastgroup_parsekey "" + foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + + if {$argumentclass eq "opts"} { + set thisgroup [dict get $arginfo -group] + if {$thisgroup ne $lastgroup} { + if {[dict exists $form_dict OPT_GROUPS $thisgroup -parsekey]} { + set thisgroup_parsekey [dict get $form_dict OPT_GROUPS $thisgroup -parsekey] + } else { + set thisgroup_parsekey "" + } + + #footer/line? + if {$use_table} { + $t add_row [list " " "" "" "" ""] + } else { + lappend errlines " " + } + + if {$thisgroup eq ""} { + } else { + #SHOW group 'header' (not really a table header - just another row) + set help "" + if {[dict exists $form_dict OPT_GROUPS $thisgroup -help]} { + set help [dict get $form_dict OPT_GROUPS $thisgroup -help] + } + if {$thisgroup_parsekey eq ""} { + set groupinfo "(documentation group)" + } else { + set groupinfo "(common flag group)\nkey:$thisgroup_parsekey" + } + if {$use_table} { + $t add_row [list " $thisgroup" $groupinfo "" "" $help] + if {$arg eq $badarg} { + $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG + } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG + } + } else { + #review - formatting will be all over the shop due to newlines in typesshow, help + #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" + set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] + append arghelp \n + if {$arg eq $badarg} { + set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] + } elseif {$arg in $goodargs} { + set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] + } + foreach ln [split $help \n] { + append arghelp " $ln" \n + } + lappend errlines $arghelp + } + } + set lastgroup $thisgroup + set lastgroup_parsekey $thisgroup_parsekey + } + if {[dict exists $arginfo -parsekey]} { + set mypkey [dict get $arginfo -parsekey] + if {$mypkey eq "$lastgroup_parsekey" || $mypkey eq [string trimright [lindex [split $arg |] end] =]} { + set hint "" + } + } + } + if {[dict exists $arginfo -default]} { set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" } else { @@ -3577,14 +3903,15 @@ tcl::namespace::eval punk::args { } else { set shortestid [dict get $idents $c] } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } + lassign [punk::lib::string_splitbefore $c [string length $shortestid]] prefix tail + #if {$shortestid eq $c} { + # set prefix $c + # set tail "" + #} else { + # set idlen [string length $shortestid] + # set prefix [string range $c 0 $idlen-1] + # set tail [string range $c $idlen end] + #} set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] if {[llength $markers]} { set mk " [join $markers {}]" @@ -3757,12 +4084,25 @@ tcl::namespace::eval punk::args { if {[dict exists $arginfo -maxsize]} { append typeshow \n "-maxsize [dict get $arginfo -maxsize]" } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" + if {[dict exists $arginfo -typeranges]} { + set ranges [dict get $arginfo -typeranges] + if {[llength $ranges] == 1} { + append typeshow \n "-range [lindex [dict get $arginfo -typeranges] 0]" + } else { + append typeshow \n "-ranges" + foreach r $ranges { + append typeshow " {$r}" + } + } } if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] + if {$hint ne ""} { + set col1 $argshow\n$hint + } else { + set col1 $argshow + } + $t add_row [list $col1 $typeshow $default $multiple $help] if {$arg eq $badarg} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG } elseif {$arg in $goodargs} { @@ -3770,7 +4110,13 @@ tcl::namespace::eval punk::args { } } else { #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" + set linetail " TYPE:$typeshow DEFAULT:$default MULTI:$multiple" + if {$hint ne ""} { + set arghelp [textblock::join -- "[a+ bold]$argshow\n$hint$RST" $linetail] + } else { + set arghelp "[a+ bold]$argshow$RST $linetail" + } + append arghelp \n if {$arg eq $badarg} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] } elseif {$arg in $goodargs} { @@ -4084,7 +4430,10 @@ tcl::namespace::eval punk::args { }]} { #unhappy path - not enough options #review - which form of punk::args::parse? - punk::args::parse $args withid ::punk::args::parse + #we expect this to always raise error - review + set result [punk::args::parse $args withid ::punk::args::parse] + puts stderr "punk::args::parse unexpected result $result" + return ;#failsafe } incr i -1 #lappend opts $a [lindex $opts_and_vals $i] @@ -4322,35 +4671,73 @@ tcl::namespace::eval punk::args { #set v [lindex $values end-$ridx] set v [lindex $all_remaining end] set tp [lindex $typelist 0] + # ----------------- + set tp [string trim $tp ?] ;#shouldn't be necessary #review - ignore ?literal? and ?literal(xxx)? when clause-length == 1? (should raise error during definition instead? - #we shouldn't have an optional clause member if there is only one member - the whole name should be marked -optional true instead. - set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - #plain "literal" without bracketed specifier - match to argument name - set match $clausename - } - if {$v eq $match} { - set alloc_ok 1 - lpop all_remaining - if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + #we shouldn't have an optional clause member if there is only one member - the whole argument should be marked -optional true instead. + # ----------------- + + #todo - support complex type members such as -type {{literal a|b} int OR} + #for now - require llength 1 - simple type such as -type {literal(ab)|int} + if {[llength $tp] !=1} { + error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + } + + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + switch -exact -- [lindex $tp_alternative 0] { + literal { + set litinfo [string range $tp 7 end] ;#get bracketed part if of form literal(xxx) + set match [lindex $tp_alternative 1] + if {$v eq $match} { + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + #the type (or one of the possible type alternates) matched a literal + break + } } - } else { - #break + stringstartswith { + set pfx [lindex $tp_alternative 1] + if {[string match "$pfx*" $v} { + set alloc_ok 1 + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + break + } + + } + stringendswith { + set sfx [lindex $tp_alternative 1] + if {[string match "*$sfx" $v} { + set alloc_ok 1 + set alloc_ok 1 + ledit all_remaining end end + if {![dict get $ARG_INFO $clausename -multiple]} { + ledit tailnames end end + } + break + } + + } + default {} } - } else { - #break } if {!$alloc_ok} { if {![dict get $ARG_INFO $clausename -optional]} { break } } + } else { + #todo - use _split_type_expression + + #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) #This is better caught during definition. #e.g rn = {elseif expr (?then?) body} typelist = {literal expr ?literal? script} @@ -4360,12 +4747,11 @@ tcl::namespace::eval punk::args { set alloc_count 0 #clause name may have more entries than types - extras at beginning are ignored set rtypelist [lreverse $typelist] - set rclausename [lrange [lreverse $clausename] 0 [llength $typelist]-1] - #assert length of rtypelist >= $rclausename set alloc_ok 0 set reverse_type_index 0 - foreach tp $rtypelist membername $rclausename { - #(membername may be empty if not enough elements) + #todo handle type-alternates + # for example: -type {string literal(x)|literal(y)} + foreach tp $rtypelist { #set rv [lindex $rcvals end-$alloc_count] set rv [lindex $all_remaining end-$alloc_count] if {[string match {\?*\?} $tp]} { @@ -4374,52 +4760,63 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { + switch -glob $tp { + literal* { + set litinfo [string range $tp 7 end] set match [string range $litinfo 1 end-1] - } else { - #if membername empty - equivalent to "literal()" - matches empty string literal - #edgecase - possibly? no need for empty-string literals - but allow it without error. - set match $membername + #todo -literalprefix + if {$rv eq $match} { + set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok + incr alloc_count + } else { + if {$clause_member_optional} { + # + } else { + set alloc_ok 0 + break + } + } } - if {$rv eq $match} { - set alloc_ok 1 ;#we need at least one literal-match to set alloc_ok - incr alloc_count - } else { - if {$clause_member_optional} { - # + "stringstartswith(*" { + set pfx [string range $tp 17 end-1] + if {[string match "$pfx*" $tp]} { + set alloc_ok 1 + incr alloc_count } else { - set alloc_ok 0 - break + if {!$clause_member_optional} { + set alloc_ok 0 + break + } } } - } else { - if {$clause_member_optional} { - #review - optional non-literal makes things harder.. - #we don't want to do full type checking here - but we now risk allocating an item that should actually - #be allocated to the previous value - set prev_type [lindex $rtypelist $reverse_type_index+1] - if {[string match literal* $prev_type]} { - set litinfo [string range $prev_type 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] + default { + if {$clause_member_optional} { + #review - optional non-literal makes things harder.. + #we don't want to do full type checking here - but we now risk allocating an item that should actually + #be allocated to the previous value + # todo - lsearch to next literal or non-optional? + set prev_type [lindex $rtypelist $reverse_type_index+1] + if {[string match literal* $prev_type]} { + set litinfo [string range $prev_type 7 end] + #todo -literalprefix + if {[string match (*) $litinfo]} { + set match [string range $litinfo 1 end-1] + } else { + set match [lindex $rclausename $reverse_type_index+1] + } + if {$rv ne $match} { + #current val doesn't match previous type - allocate here + incr alloc_count + } } else { - #prev membername - set match [lindex $rclausename $reverse_type_index+1] - } - if {$rv ne $match} { - #current val doesn't match previous type - allocate here + #no literal to anchor against.. incr alloc_count } } else { - #no literal to anchor against.. + #allocate regardless of type - we're only matching on arity and literal positioning here. + #leave final type-checking for later. incr alloc_count } - } else { - #allocate regardless of type - we're only matching on arity and literal positioning here. - #leave final type-checking for later. - incr alloc_count } } incr reverse_type_index @@ -4430,7 +4827,8 @@ tcl::namespace::eval punk::args { set all_remaining [lrange $all_remaining 0 end-$alloc_count] #don't lpop if -multiple true if {![dict get $ARG_INFO $clausename -multiple]} { - lpop tailnames + #lpop tailnames + ledit tailnames end end } } else { break @@ -4454,13 +4852,15 @@ tcl::namespace::eval punk::args { } #thistype - set alloc_ok 1 + set alloc_ok 1 ;#default assumption only set alloc_count 0 set resultlist [list] set n [expr {[llength $thistype]-1}] - #name can have more or less items than typelist - set thisnametail [lrange $thisname end-$n end] - foreach tp $thistype membername $thisnametail { + set tpidx 0 + set newtypelist $thistype + set has_choices [expr {[tcl::dict::exists $ARG_INFO $thisname -choices] || [tcl::dict::exists $ARG_INFO $thisname -choicegroups]}] + foreach tp $thistype { + #usual case is a single tp (basic length-1 clause) - but tp may commonly have alternates eg int|literal(xxx) set v [lindex $all_remaining $alloc_count] if {[string match {\?*\?} $tp]} { set clause_member_optional 1 @@ -4468,56 +4868,1884 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - if {[string match literal* $tp]} { - set litinfo [string range $tp 7 end] - if {[string match (*) $litinfo]} { - set match [string range $litinfo 1 end-1] - } else { - set match $membername - } - if {$v eq $match} { - if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { - lappend resultlist "" - } else { - lappend resultlist $v - incr alloc_count + + set member_satisfied 0 + if {$has_choices} { + #each tp in the clause is just for validating a value outside the choice-list when -choicerestricted 0 + set member_satisfied 1 + } + + + if {!$member_satisfied} { + #----------------------------------------------------------------------------------- + #first build category lists of any literal,literalprefix,stringstartwith,other + # + set ctg_literals [list] + set ctg_literalprefixes [list] + set ctg_stringstartswith [list] + set ctg_stringendswith [list] + set ctg_other [list] + #foreach tp_alternative [split $tp |] {} + foreach tp_alternative [_split_type_expression $tp] { + #JJJJ + lassign $tp_alternative t textra + switch -exact -- $t { + literal { + lappend ctg_literals $textra + } + literalprefix { + lappend ctg_literalprefixes $textra + } + stringstartswith { + lappend ctg_stringstartswith $textra + } + stringendswith { + lappend ctg_stringendswith $textra + } + default { + lappend ctg_other $tp_alternative + } } - } else { - if {$clause_member_optional} { - #todo - configurable default for optional clause members? - lappend resultlist "" - } else { - set alloc_ok 0 + } + #----------------------------------------------------------------------------------- + if {[llength $ctg_other] > 0} { + #presence of any ordinary type as one of the alternates - means we consider it a match if certain basic types align + #we don't do full validation here -leave main validation for later (review) + foreach tp_alternative $ctg_other { + switch -exact -- $tp_alternative { + int { + if {[string is integer -strict $v]} { + set member_satisfied 1 + break + } + } + double { + if {[string is double -strict $v]} { + set member_satisfied 1 + break + } + } + bool { + if {[string is boolean -strict $v]} { + set member_satisfied 1 + break + } + } + number { + if {[string is integer -strict $v] || [string is double -strict $v]} { + set member_satisfied 1 + break + } + } + dict { + if {[punk::args::lib::string_is_dict $v]} { + set member_satisfied 1 + break + } + } + default { + #REVIEW!!! + #can get infinite loop in get_dict if not satisfied - unstoppable until memory exhausted. + #todo - catch/detect in caller + set member_satisfied 1 + break + } + } + } + } + } + + if {!$member_satisfied && ([llength $ctg_literals] || [llength $ctg_literalprefixes])} { + if {$v in $ctg_literals} { + set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + } else { + #ctg_literals is included in the prefix-calc - but a shortened version of an entry in literals is not allowed + #(exact match would have been caught in other branch of this if) + #review - how does ctg_stringstartswith affect prefix calc for literals? + set full_v [tcl::prefix::match -error "" [list {*}$ctg_literals {*}$ctg_literalprefixes] $v] + if {$full_v ne "" && $full_v ni $ctg_literals} { + #matched prefix must be for one of the entries in ctg_literalprefixes - valid + set member_satisfied 1 + set v $full_v ;#map prefix given as arg to the full literalprefix value + lset newtypelist $tpidx validated-$tp + } + } + } + if {!$member_satisfied && [llength $ctg_stringstartswith]} { + foreach pfx $ctg_stringstartswith { + if {[string match "$pfx*" $v]} { + set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + #review. consider multi-word typespec with RPN? + # {*}$tp_alternative validated break } } - } else { - if {$clause_member_optional} { - if {$alloc_count >= [llength $all_remaining]} { - lappend resultlist "" + } + if {!$member_satisfied && [llength $ctg_stringendswith]} { + foreach pfx $ctg_stringendswith { + if {[string match "*$pfx" $v]} { + set member_satisfied 1 + lset newtypelist $tpidx validated-$tp + break + } + } + } + + + + if {$member_satisfied} { + if {$clause_member_optional && $alloc_count >= [llength $all_remaining]} { + if {[dict exists $ARG_INFO $thisname -typedefaults]} { + set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] + lappend resultlist $d + lset newtypelist $tpidx ?defaulted-$tp? } else { - lappend resultlist $v - incr alloc_count + lset newtypelist $tpidx ?omitted-$tp? + lappend resultlist "" } } else { + #may have satisfied one of the basic type tests above lappend resultlist $v incr alloc_count } + } else { + if {$clause_member_optional} { + if {[dict exists $ARG_INFO $thisname -typedefaults]} { + set d [lindex [dict get $ARG_INFO $thisname -typedefaults] $tpidx] + lappend resultlist $d + lset newtypelist $tpidx ?defaulted-$tp? + } else { + lappend resultlist "" + lset newtypelist $tpidx ?omitted-$tp? + } + } else { + set alloc_ok 0 + } } + if {$alloc_count > [llength $all_remaining]} { set alloc_ok 0 break } + incr tpidx } + + #?omitted-*? and ?defaulted-*? in typelist are a way to know which elements in the clause were missing/defaulted + #so that they are not subject to type validation + #such elements shouldn't be subject to validation if {$alloc_ok} { - set d [dict create consumed $alloc_count resultlist $resultlist] + #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - set d [dict create consumed 0 resultlist {}] + puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + set d [dict create consumed 0 resultlist {} typelist $thistype] } #puts ">>>> _get_dict_can_assign_value $d" return $d } + #_split_type_expression + #only handles toplevel 'or' for type_expression e.g int|char + #we have no mechanism for & - (although it would be useful) + #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) + #or perhaps more performant, RPN to avoid bracket parsing + #if literal(..), literalprefix(..), stringstartswith(..) etc can have pipe symbols and brackets etc - we can't just use split + #if we require -type to always be treated as a list - and if an element is length 1 - require it to + #have properly balanced brackets that don't contain | ( ) etc we can simplify - REVIEW + + #consider: + #1 basic syntax - only OR supported - limits on what chars can be put in 'textn' elements. + #mode -type literalprefix(text1)|literalprefix(text2) -optional 1 + #2 expanded syntax - supports arbitrary chars in 'textn' elements - but still doesn't support more complex OR/AND logic + #mode -type {{literalprefix text1 | literalprefix text2}} + #3 RPN (reverse polish notation) - somewhat unintuitive, but allows arbitrary textn, and complex OR/AND logic without brackets. + #(forth like - stack based definition of types) + #mode -type {literalprefix text1 literalprefix text2 OR} + #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + + proc _split_type_expression {type_expression} { + if {[llength $type_expression] == 1} { + #simple expressions of length one must be splittable on | + #disallowed: things such as literal(|) or literal(x|etc)|int + #these would have to be expressed as {literal |} and {literal x|etc | int} + set or_type_parts [split $type_expression |] + set type_alternatives [list] + foreach t $or_type_parts { + if {[regexp {([^\(^\)]*)\((.*)\)$} $t _ name val]} { + lappend type_alternatives [list $name $val] + } else { + lappend type_alternatives $t + } + } + return $type_alternatives + } else { + error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + #todo + #RPN reverse polish notation + #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} + #equivalent logic: ((stringstartswith(x)|stringstartswith(y))&stringendswith(z))|int + # {int ; stringstartswith x stringstartswith y OR } + + #experimental.. seems like a pointless syntax. + #may as well just use list of lists with |(or) as the intrinsic operator instead of parsing this + #e.g {stringstartswith x | literal | | int} + set type_alternatives [list] + set expect_separator 0 + for {set w 0} {$w < [llength $type_expression]} {incr w} { + set word [lindex $type_expression $w] + if {$expect_separator} { + if {$word eq "|"} { + #pipe could be last entry - not strictly correct, but can ignore + set expect_separator 0 + continue + } else { + error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + } + } + switch -exact -- $word { + literal - literalprefix - stringstartswith - stringendswith - stringcontains { + if {$w+1 > [llength $type_expression]} { + #premature end - no arg available for type which requires one + error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + } + lappend type_alternatives [list $word [lindex $type_expression $w+1]] + incr w ;#consume arg + set expect_separator 1 + } + default { + #simple types such as int,double,string + lappend type_alternatives $word + set expect_separator 1 + } + } + } + return $type_alternatives + } + } + + #old version + ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ### #set type $type_expression ;#todo - 'split' on | + ### set vlist $clausevalues_raw + ### set vlist_check $clausevalues_check + + ### set type_alternatives [_split_type_expression $type_expression] + ### #each type_alternative is a list of varying length depending on arguments supported by first word. + ### #TODO? + ### #single element types: int double string etc + ### #two element types literal literalprefix stringstartswith stringendswith + ### #TODO + ### set stype [lindex $type_alternatives 0] + ### #e.g int + ### #e.g {literal blah)etc} + ### set type [lindex $stype 0] + ### #switch on first word of each stype + ### # + + ### #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + ### switch -- $type { + ### any {} + ### literal { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {$e ne $testval} { + ### set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### stringstartwith { + ### foreach clauseval $vlist { + ### set e [lindex $clauseval $clausecolumn] + ### set testval [lindex $stype 1] + ### if {![string match $testval* $e]} { + ### set msg "$argclass '$argname' for %caller% requires stringstartswith value '$argname'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### list { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is list -strict $e_check]} { + ### set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### tcl::dict::for {checkopt checkval} $thisarg_checks { + ### switch -- $checkopt { + ### -minsize { + ### # -1 for disable is as good as zero + ### if {[llength $e_check] < $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### -maxsize { + ### if {$checkval ne "-1"} { + ### if {[llength $e_check] > $checkval} { + ### set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### } + ### indexexpression { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[catch {lindex {} $e_check}]} { + ### set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### string - ansistring - globstring { + ### #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + ### #we possibly don't want to always have to regex on things that don't pass the other more basic checks + ### # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + ### # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + ### # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + ### # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + ### # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + ### # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + ### #todo? - way to validate both unstripped and stripped? + ### set pass_quick_list_e [list] + ### set pass_quick_list_e_check [list] + ### set remaining_e $vlist + ### set remaining_e_check $vlist_check + ### #review - order of -regexprepass and -regexprefail in original rawargs significant? + ### #for now -regexprepass always takes precedence + ### set regexprepass [tcl::dict::get $thisarg -regexprepass] + ### set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + ### if {$regexprepass ne ""} { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + ### lappend pass_quick_list_e $clauseval + ### lappend pass_quick_list_e_check $clauseval_check + ### } + ### } + ### set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] + ### set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] + ### } + ### if {$regexprefail ne ""} { + ### foreach clauseval $remaining_e clauseval_check $remaining_e_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #puts "----> checking $e vs regex $regexprefail" + ### if {[regexp $regexprefail $e]} { + ### if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + ### #review - %caller% ?? + ### set msg [tcl::dict::get $thisarg -regexprefailmsg] + ### } else { + ### set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + ### } + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### switch -- $type { + ### ansistring { + ### #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + ### #.. so we need to look at the original values in $vlist not $vlist_check + + ### #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + ### #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + ### package require punk::ansi + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![punk::ansi::ta::detect $e]} { + ### set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### globstring { + ### foreach clauseval $remaining_e { + ### set e [lindex $clauseval $clausecolumn] + ### if {![regexp {[*?\[\]]} $e]} { + ### set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + + ### if {[tcl::dict::size $thisarg_checks]} { + ### foreach clauseval $remaining_e_check { + ### set e_check [lindex $clauseval $clausecolumn] + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsize [dict get $thisarg_checks -minsize] + ### # -1 for disable is as good as zero + ### if {[tcl::string::length $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsize [dict get $thisarg_checks -maxsize] + ### if {$checkval ne "-1"} { + ### if {[tcl::string::length $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### number { + ### #review - consider effects of Nan and Inf + ### #NaN can be considered as 'technically' a number (or at least a special numeric value) + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign {} low high ;#set both empty + ### lassign $range low high + + ### if {"$low$high" ne ""} { + ### if {[::tcl::mathfunc::isnan $e]} { + ### set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$low eq ""} { + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### int { + ### #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is integer -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::exists $thisarg -typeranges]} { + ### set ranges [tcl::dict::get $thisarg -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### lassign $range low high + ### if {"$low$high" ne ""} { + ### if {$low eq ""} { + ### #lowside unspecified - check only high + ### if {$e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } elseif {$high eq ""} { + ### #highside unspecified - check only low + ### if {$e_check < $low} { + ### set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } else { + ### #high and low specified + ### if {$e_check < $low || $e_check > $high} { + ### set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### double { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is double -strict $e_check]} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -typeranges]} { + ### set ranges [dict get $thisarg_checks -typeranges] + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set range [lindex $ranges $clausecolumn] + ### #todo - small-value double comparisons with error-margin? review + ### #todo - empty string for low or high + ### lassign $range low high + ### if {$e_check < $low || $e_check > $high} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### bool { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is boolean -strict $e_check]} { + ### set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### dict { + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[llength $e_check] %2 != 0} { + ### set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### if {[tcl::dict::size $thisarg_checks]} { + ### if {[dict exists $thisarg_checks -minsize]} { + ### set minsizes [dict get $thisarg_checks -minsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set minsize [lindex $minsizes $clausecolumn] + ### # -1 for disable is as good as zero + ### if {[tcl::dict::size $e_check] < $minsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### if {[dict exists $thisarg_checks -maxsize]} { + ### set maxsizes [dict get $thisarg_checks -maxsize] + ### foreach clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### set maxsize [lindex $maxsizes $clausecolumn] + ### if {$maxsize ne "-1"} { + ### if {[tcl::dict::size $e_check] > $maxsize} { + ### set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### } + ### } + ### alnum - + ### alpha - + ### ascii - + ### control - + ### digit - + ### graph - + ### lower - + ### print - + ### punct - + ### space - + ### upper - + ### wordchar - + ### xdigit { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {![tcl::string::is $type -strict $e_check]} { + ### set e [lindex $clauseval $t] + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### file - + ### directory - + ### existingfile - + ### existingdirectory { + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e [lindex $clauseval $clausecolumn] + ### set e_check [lindex $clauseval_check $clausecolumn] + ### #//review - we may need '?' char on windows + ### if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + ### #what about special file names e.g on windows NUL ? + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + ### } + ### if {$type eq "existingfile"} { + ### if {![file exists $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } elseif {$type eq "existingdirectory"} { + ### if {![file isdirectory $e_check]} { + ### set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### } + ### char { + ### #review - char vs unicode codepoint vs grapheme? + ### foreach clauseval $vlist clauseval_check $vlist_check { + ### set e_check [lindex $clauseval_check $clausecolumn] + ### if {[tcl::string::length $e_check] != 1} { + ### set e [lindex $clauseval $clausecolumn] + ### set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + ### return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + ### } + ### } + ### } + ### default { + ### } + ### } + + ###} + + #new version + #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname + #common basic case: list of single item being a single value clause. + #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} + proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + #default_type_expression is for the chosen clausecolumn + #if {$argname eq "frametype"} { + #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" + #puts "--->list_of_clauses_raw : $list_of_clauses_raw" + #puts "--->list_of_clauses_check: $list_of_clauses_check" + #puts "--->$argname -type: [dict get $thisarg -type]" + #} + + set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn + + set default_type_alternatives [_split_type_expression $default_type_expression] + #--------------------- + #pre-calc prefix sets based on the default. + set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] + set literals [lmap v $alt_literals {lindex $v 1}] + set alt_literalprefixes [lsearch -all -inline -index 0 $default_type_alternatives literalprefix] + set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + #--------------------- + + #each type_alternative is a list of varying length depending on arguments supported by first word. + #TODO? + #single element types: int double string etc + #two element types literal literalprefix stringstartswith stringendswith + #TODO + + #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $default_type_alternatives] _]] + #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + #-types {int|char|literal(ok) char double} + #we are checking a and 1 against the defaulttype_expression e.g int|char|literal(ok) (type_alternatives = {int char literal(ok)} + #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + #review: for a particular clause the active type_expression might be overridden with 'any' if the column has already passed a -choices test + # + + set e_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_raw *] + set check_vals [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] + set typelist_vals_raw [lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_types *] + set typelist_vals [lmap v $typelist_vals_raw {string trim $v ?}] + + set c_idx -1 + foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { + incr c_idx + set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set firstany [lsearch -exact $col_type_alternatives any] + if {$firstany > -1} { + lset clause_results $c_idx $firstany 1 + continue + } + set a_idx -1 + foreach typealt $col_type_alternatives { + incr a_idx + lassign $typealt type testval ;#testval will be empty for basic types, but applies to literal, literalprefix, stringstartswith etc. + switch -exact -- $type { + literal { + if {$e ne $testval} { + set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } + } + literalprefix { + #this specific literalprefix testval value not relevant - we're testing against all in the set of typealternates + set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + if {$match ne "" && $match ni $literals} { + lset clause_results $c_idx $a_idx 1 + #this clause is satisfied - no need to process it for other typealt + break + } else { + set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringstartswith { + if {[string match $testval* $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + stringendswith { + if {[string match *$testval $e]} { + lset clause_results $c_idx $a_idx 1 + break + } else { + set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + } + } + list { + if {![tcl::string::is list -strict $e_check]} { + set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + continue + } else { + if {[dict exists $thisarg_checks -minsize]} { + # -1 for disable is as good as zero + set minsize [dict get $thisarg_checks -minsize] + if {[llength $e_check] < $minsize} { + set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exist $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$maxsize ne "-1"} { + if {[llength $e_check] > $maxsize} { + set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + indexexpression { + if {[catch {lindex {} $e_check}]} { + set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + string - ansistring - globstring { + #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + #todo? - way to validate both unstripped and stripped? + #review - order of -regexprepass and -regexprefail in original rawargs significant? + #for now -regexprepass always takes precedence + #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + set regexprepass [tcl::dict::get $thisarg -regexprepass] + set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + if {$regexprepass ne ""} { + if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + lset clause_results $c_idx $a_idx 1 + break + } + } + if {$regexprefail ne ""} { + #puts "----> checking $e vs regex $regexprefail" + if {[regexp $regexprefail $e]} { + if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + #review - %caller% ?? + set msg [tcl::dict::get $thisarg -regexprefailmsg] + } else { + set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + } + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + #review - tests? + continue + } + } + switch -- $type { + ansistring { + #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + package require punk::ansi + if {![punk::ansi::ta::detect $e]} { + set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + globstring { + if {![regexp {[*?\[\]]} $e]} { + set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsize [dict get $thisarg_checks -minsize] + # -1 for disable is as good as zero + if {[tcl::string::length $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [dict get $thisarg_checks -maxsize] + if {$checkval ne "-1"} { + if {[tcl::string::length $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + number { + #review - consider effects of Nan and Inf + #NaN can be considered as 'technically' a number (or at least a special numeric value) + if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign {} low high ;#set both empty + lassign $range low high + if {"$low$high" ne ""} { + if {[::tcl::mathfunc::isnan $e]} { + set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {$low eq ""} { + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + int { + #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + if {![tcl::string::is integer -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg -typeranges]} { + set ranges [tcl::dict::get $thisarg -typeranges] + set range [lindex $ranges $clausecolumn] + lassign $range low high + if {"$low$high" ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } else { + #high and low specified + if {$e_check < $low || $e_check > $high} { + set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + double { + if {![tcl::string::is double -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + if {[tcl::dict::exists $thisarg_checks -typeranges]} { + set ranges [dict get $thisarg_checks -typeranges] + set range [lindex $ranges $clausecolumn] + #todo - small-value double comparisons with error-margin? review + lassign $range low high + if {$low$high ne ""} { + if {$low eq ""} { + #lowside unspecified - check only high + if {$e_check > $high} { + set msg "$argclass $argname for %caller% must be double less than or equal to $high. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$high eq ""} { + #highside unspecified - check only low + if {$e_check < $low} { + set msg "$argclass $argname for %caller% must be double greater than or equal to $low. Received: '$e'" + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } elseif {$e_check < $low || $e_check > $high} { + set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + bool { + if {![tcl::string::is boolean -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + dict { + if {[llength $e_check] %2 != 0} { + set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {[tcl::dict::size $thisarg_checks]} { + if {[dict exists $thisarg_checks -minsize]} { + set minsizes [dict get $thisarg_checks -minsize] + set minsize [lindex $minsizes $clausecolumn] + # -1 for disable is as good as zero + if {[tcl::dict::size $e_check] < $minsize} { + set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type minsize $minsize] msg $msg] + continue + } + } + if {[dict exists $thisarg_checks -maxsize]} { + set maxsize [lindex $maxsizes $clausecolumn] + if {$maxsize ne "-1"} { + if {[tcl::dict::size $e_check] > $maxsize} { + set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + lset clause_results $c_idx $a_idx [list err [list sizeviolation $type maxsize $maxsize] msg $msg] + continue + } + } + } + } + lset clause_results $c_idx $a_idx 1 + } + alnum - + alpha - + ascii - + control - + digit - + graph - + lower - + print - + punct - + space - + upper - + wordchar - + xdigit { + if {![tcl::string::is $type -strict $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + file - + directory - + existingfile - + existingdirectory { + #//review - we may need '?' char on windows + if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + #what about special file names e.g on windows NUL ? + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + if {$type eq "existingfile"} { + if {![file exists $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } elseif {$type eq "existingdirectory"} { + if {![file isdirectory $e_check]} { + set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + lset clause_results $c_idx $a_idx 1 + } + char { + #review - char vs unicode codepoint vs grapheme? + if {[tcl::string::length $e_check] != 1} { + set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } else { + lset clause_results $c_idx $a_idx 1 + break + } + } + tk_screen_units { + switch -exact -- [string index $e_check end] { + c - i - m - p { + set numpart [string range $e_check 0 end-1] + if {![tcl::string::is double $numpart]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + default { + if {![tcl::string::is double $e_check]} { + set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] + continue + } + } + } + lset clause_results $c_idx $a_idx 1 + break + } + default { + #default pass for unrecognised types - review. + lset clause_results $c_idx $a_idx 1 + break + } + } + } + } + + foreach clauseresult $clause_results { + if {[lsearch $clauseresult 1] == -1} { + #no pass for this clause - fetch first? error and raise + #todo - return error containing clause_indices so we can report more than one failing element at once? + foreach e $clauseresult { + switch -exact [lindex $e 0] { + errorcode { + #errorcode msg checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against type_expression $type_expression" + # puts "--->list_of_clauses_raw : $list_of_clauses_raw" + # puts "--->list_of_clauses_check: $list_of_clauses_check" + # puts "--->$argname -type: [dict get $thisarg -type]" + # } + + # set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just passed type_expression + + # #set vlist [list] + # set clauses_dict [dict create] ;#key is ordinal position, remove entries as they are satsified + # set cidx -1 + # foreach cv $list_of_clauses_raw { + # incr cidx + # #REVIEW + # #if {$clause_size ==1} { + # # lappend vlist [list $cidx [list $cv]] + # #} else { + # #lappend vlist [list $cidx $cv] ;#store the index so we can reduce vlist as we go + # dict set clauses_dict $cidx $cv + # #} + # } + # #set vlist_check [list] + # set clauses_dict_check [dict create] + # set cidx -1 + # foreach cv $list_of_clauses_check { + # incr cidx + # #if {$clause_size == 1} { + # # lappend vlist_check [list $cidx [list $cv]] + # #} else { + # #lappend vlist_check [list $cidx $cv] + # dict set clauses_dict_check $cidx $cv + # #} + # } + + # set type_alternatives [_split_type_expression $type_expression] + # #each type_alternative is a list of varying length depending on arguments supported by first word. + # #TODO? + # #single element types: int double string etc + # #two element types literal literalprefix stringstartswith stringendswith + # #TODO + + # #list for each clause (each clause is itself a list - usually length 1 but can be any length - we are dealing only with one column of the clauses) + # set clause_results [lrepeat [llength $list_of_clauses_raw] [lrepeat [llength $type_alternatives] _]] + # #e.g for list_of_clauses_raw {{a b c} {1 2 3}} when clausecolumn is 0 + # #-types {int|char|literal(ok) char double} + # #we are checking a and 1 against the type_expression int|char|literal(ok) (type_alternatives = {int char literal(ok)} + # #our initial clause_results in this case is a 2x2 list {{_ _ _} {_ _ _}} + # # + + + # set a_idx -1 + # foreach typealt $type_alternatives { + # incr a_idx + + # set type [lindex $typealt 0] + # #e.g int + # #e.g {literal blah} + # #e.g {literalprefix abc} + + # #switch on first word of each typealt + # # + + # #review - for leaders,values - do we need to check literal etc? already checked during split into prevalues postvalues ? + # switch -- $type { + # any {} + # literal { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {$e ne $testval} { + # set msg "$argclass '$argname' for %caller% requires literal value '$testval'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # literalprefix { + # set alt_literals [lsearch -all -inline -index 0 $type_alternatives literal] + # set literals [lmap v $alt_literals {lindex $v 1}] + # set alt_literalprefixes [lsearch -all -inline -index 0 $type_alternatives literalprefix] + # set literalprefixes [lmap v $alt_literalprefixes {lindex $v 1}] + + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # #this specific literalprefix value not relevant - we're testing against all in the set of typealternates + # #set testval [lindex $typealt 1] + # set match [::tcl::prefix::match -error "" [list {*}$literals {*}$literalprefixes] $e] + # if {$match ne "" && $match ni $literals} { + # lset clause_results $c_idx $a_idx 1 + # #this clause is satisfied - no need to process it for other typealt + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires unambiguous literal prefix match for one of '$literalprefixes' within prefix calculation set:'[list {*}$literals {*}$literalprefixes]'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringstartswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match $testval* $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringstartswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # stringendswith { + # dict for {c_idx clauseval} $clauses_dict { + # set e [lindex $clauseval $clausecolumn] + # set testval [lindex $typealt 1] + # if {[string match *$testval $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } else { + # set msg "$argclass '$argname' for %caller% requires stringendswith value '$testval'. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs] msg $msg] + # } + # } + # } + # list { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # if {![tcl::string::is list -strict $e_check]} { + # set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } else { + # if {[dict exists $thisarg_checks -minsize]} { + # # -1 for disable is as good as zero + # set minsize [dict get $thisarg_checks -minsize] + # if {[llength $e_check] < $minsize} { + # set msg "$argclass '$argname for %caller% requires list with -minsize $minsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exist $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$maxsize ne "-1"} { + # if {[llength $e_check] > $maxsize} { + # set msg "$argclass '$argname for %caller% requires list with -maxsize $maxsize. Received len:[llength $e_check]" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $e_check -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # indexexpression { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[catch {lindex {} $e_check}]} { + # set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # string - ansistring - globstring { + # #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string + # #we possibly don't want to always have to regex on things that don't pass the other more basic checks + # # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) + # # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) + # # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead + # # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function + # # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) + # # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail + + # #todo? - way to validate both unstripped and stripped? + # #review - order of -regexprepass and -regexprefail in original rawargs significant? + # #for now -regexprepass always takes precedence + # #REVIEW we only have a single regexprepass/regexprefail for entire typeset?? need to make it a list like -typedefaults? + # set regexprepass [tcl::dict::get $thisarg -regexprepass] + # set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 + # if {$regexprepass ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[regexp [lindex $regexprepass $clausecolumn] $e]} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # if {$regexprefail ne ""} { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # #puts "----> checking $e vs regex $regexprefail" + # if {[regexp $regexprefail $e]} { + # if {[tcl::dict::exists $thisarg -regexprefailmsg]} { + # #review - %caller% ?? + # set msg [tcl::dict::get $thisarg -regexprefailmsg] + # } else { + # set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" + # } + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs] msg $msg] + # #review - tests? + # } + # } + # } + # switch -- $type { + # ansistring { + # #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi + # #.. so we need to look at the original values in $clauses_dict not $clauses_dict_check + + # #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? + # #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? + # package require punk::ansi + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![punk::ansi::ta::detect $e]} { + # set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # globstring { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # if {![regexp {[*?\[\]]} $e]} { + # set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # } + # } + + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # if {[tcl::dict::size $thisarg_checks]} { + # set passed_checks 1 + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[dict exists $thisarg_checks -minsize]} { + # set minsize [dict get $thisarg_checks -minsize] + # # -1 for disable is as good as zero + # if {[tcl::string::length $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set maxsize [dict get $thisarg_checks -maxsize] + # if {$checkval ne "-1"} { + # if {[tcl::string::length $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } else { + # if {[lindex $clause_results $c_idx $a_idx] eq "_"} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # number { + # #review - consider effects of Nan and Inf + # #NaN can be considered as 'technically' a number (or at least a special numeric value) + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign {} low high ;#set both empty + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {[::tcl::mathfunc::isnan $e]} { + # set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$low eq ""} { + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict usnet clauses_dict_check $c_idx + # } + # } + + # } + # int { + # #elements in -typeranges can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is integer -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg -typeranges]} { + # set ranges [tcl::dict::get $thisarg -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # lassign $range low high + # set passed_checks 1 + # if {"$low$high" ne ""} { + # if {$low eq ""} { + # #lowside unspecified - check only high + # if {$e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$high eq ""} { + # #highside unspecified - check only low + # if {$e_check < $low} { + # set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } else { + # #high and low specified + # if {$e_check < $low || $e_check > $high} { + # set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # double { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is double -strict $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type double. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } + # } + # if {[tcl::dict::exists $thisarg_checks -typeranges]} { + # set ranges [dict get $thisarg_checks -typeranges] + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set e_check [lindex $clauseval_check $clausecolumn] + # set range [lindex $ranges $clausecolumn] + # #todo - small-value double comparisons with error-margin? review + # #todo - empty string for low or high + # set passed_checks 1 + # lassign $range low high + # if {$low$high ne ""} { + # if {$e_check < $low || $e_check > $high} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } else { + # dict for {c_idx clauseval} $clauses_dict { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # bool { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is boolean -strict $e_check]} { + # set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # dict { + # dict for {c_idx clauseval_check} $clauses_dict_check { + # puts "check_clausecolumn2 dict handler: c_idx:$c_idx clausecolumn:$clausecolumn clauseval_check:$clauseval_check" + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[llength $e_check] %2 != 0} { + # set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # } + # } + # dict for {c_idx clauseval_check} $clauses_dict_check { + # if {[lindex $clause_results $c_idx $a_idx] ne "_"} { + # continue + # } + # set passed_checks 1 + # if {[tcl::dict::size $thisarg_checks]} { + # if {[dict exists $thisarg_checks -minsize]} { + # set minsizes [dict get $thisarg_checks -minsize] + # set e_check [lindex $clauseval_check $clausecolumn] + # set minsize [lindex $minsizes $clausecolumn] + # # -1 for disable is as good as zero + # if {[tcl::dict::size $e_check] < $minsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # if {$passed_checks && [dict exists $thisarg_checks -maxsize]} { + # set e_check [lindex $clauseval_check $clausecolumn] + # set maxsize [lindex $maxsizes $clausecolumn] + # if {$maxsize ne "-1"} { + # if {[tcl::dict::size $e_check] > $maxsize} { + # set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # } + + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # alnum - + # alpha - + # ascii - + # control - + # digit - + # graph - + # lower - + # print - + # punct - + # space - + # upper - + # wordchar - + # xdigit { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + # set e_check [lindex $clauseval_check $clausecolumn] + # if {![tcl::string::is $type -strict $e_check]} { + # set e [lindex $clauseval $t] + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # file - + # directory - + # existingfile - + # existingdirectory { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e [lindex $clauseval $clausecolumn] + # set e_check [lindex $clauseval_check $clausecolumn] + + # #//review - we may need '?' char on windows + # set passed_checks 1 + # if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { + # #what about special file names e.g on windows NUL ? + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # if {$passed_checks} { + # if {$type eq "existingfile"} { + # if {![file exists $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } elseif {$type eq "existingdirectory"} { + # if {![file isdirectory $e_check]} { + # set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # char { + # #review - char vs unicode codepoint vs grapheme? + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # if {[tcl::string::length $e_check] != 1} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" + # #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # } else { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # tk_screen_units { + # dict for {c_idx clauseval} $clauses_dict { + # set clauseval_check [dict get $clauses_dict_check $c_idx] + + # set e_check [lindex $clauseval_check $clausecolumn] + # set passed_checks 1 + # switch -exact -- [string index $e_check end] { + # c - i - m - p { + # set numpart [string range $e_check 0 end-1] + # if {![tcl::string::is double $numpart]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # default { + # if {![tcl::string::is double $e_check]} { + # set e [lindex $clauseval $clausecolumn] + # set msg "$argclass $argname for %caller% requires type 'tk_screen_units'. Received: '$e' Which does not seem to be in a form as accepted ty Tk_GetPixels." + # lset clause_results $c_idx $a_idx [list errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs] msg $msg] + # set passed_checks 0 + # } + # } + # } + # if {$passed_checks} { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # default { + # #default pass for unrecognised types - review. + # dict for {c_idx clauseval} $clauses_dict { + # lset clause_results $c_idx $a_idx 1 + # dict unset clauses_dict $c_idx + # dict unset clauses_dict_check $c_idx + # } + # } + # } + # } + # foreach clauseresult $clause_results { + # if {[lsearch $clauseresult 1] == -1} { + # #no pass for this clause - fetch first? error and raise + # #todo - return error containing clause_indices so we can report more than one failing element at once? + # foreach e $clauseresult { + # if {[lindex $e 0] eq "errorcode"} { + # #errorcode msg remaining_rawargs: $remaining_rawargs" #} + + set can_have_leaders 1 ;#default assumption + if {$LEADER_MAX == 0 || ([llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED)} { + set can_have_leaders 0 + } + + #REVIEW - this attempt to classify leaders vs opts vs values doesn't account for leaders with clauses containing optional elements + #e.g @leaders {x -type {int ?int?}} set nameidx 0 - if {$LEADER_MAX != 0} { - for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { - set r [lindex $rawargs $ridx] - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $nameidx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > $named_leader_args_max-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string - } - if {$OPT_MAX ne "0"} { - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) + if {$can_have_leaders} { + if {$LEADER_TAKEWHENARGSMODULO} { + #assign set of leaders purely based on number of total args + set take [expr {[llength $remaining_rawargs] % $LEADER_TAKEWHENARGSMODULO}] + set pre_values [lrange $remaining_rawargs 0 $take-1] + set remaining_rawargs [lrange $remaining_rawargs $take end] + } else { + #greedy taking of leaders based on type-matching + + set leadernames_seen [list] + for {set ridx 0} {$ridx < [llength $rawargs]} {incr ridx} { + set raw [lindex $rawargs $ridx] ;#received raw arg + if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { break } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $all_opts $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break + if {[llength $LEADER_NAMES] && $nameidx == [llength $LEADER_NAMES]-1} { + #at last named leader + set leader_posn_name [lindex $LEADER_NAMES $nameidx] + if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { + set is_multiple 1 } - if {$leader_posn_name ne ""} { - #false alarm - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - #incr ridx - continue - } else { - break + } elseif {$ridx > $named_leader_args_max-1} { + #beyond names - retain name if -multiple was true + if {!$is_multiple} { + set leader_posn_name "" } + } else { + set leader_posn_name [lindex $LEADER_NAMES $nameidx] ;#may return empty string } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" - set clauselength [llength $leader_posn_name] - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $remaining_rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop remaining_rawargs 0] - # incr ridx - # continue - # } - #} - if {[llength $remaining_rawargs] < $clauselength} { - #not enough remaining args to fill even this optional leader - #rather than raise error here - perform our break (for end of leaders) and let the code below handle it - break + if {$OPT_MAX ne "0" && [string match -* $raw]} { + #all_opts includes end_of_opts marker -- if configured - no need to explicitly check for it separately + set possible_flagname $raw + if {[string match --* $raw]} { + set eposn [string first = $raw] + # --flag=xxx + if {$eposn >=3} { + set possible_flagname [string range $raw 0 $eposn-1] + } } - - #check if enough remaining_rawargs to fill any required values - if {$valmin > 0 && [llength $remaining_rawargs] - $clauselength < $valmin} { + set matchopt [::tcl::prefix::match -error {} $all_opts $possible_flagname] + if {$matchopt ne ""} { + #flaglike matches a known flag - don't treat as leader break } + } - #leadername may be a 'clause' of arbitrary length (e.g {"key val"} or {"key val etc"}) - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx + #for each branch - break or lappend + if {$leader_posn_name ne ""} { + set leader_type [dict get $ARG_INFO $leader_posn_name -type] + #todo - variable clauselengths e.g 'if' command which has optional 'then' and 'else' "noise words" + set clauselength [llength $leader_type] + set min_clauselength 0 + foreach t $leader_type { + if {![string match {\?*\?} $t]} { + incr min_clauselength + } } - dict incr leader_posn_names_assigned $leader_posn_name - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {[llength $remaining_rawargs] < $clauselength} { + if {$leader_posn_name ni $LEADER_REQUIRED} { + #optional leader + + #most adhoc arg processing will allocate based on number of args rather than matching choice values first + #(because a choice value could be a legitimate data value) + + #review - option to process in this manner? + #first check if the optional leader value is a match for a choice ? + #if {[dict exists $arg_info $leader_posn_name -choices]} { + # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] $raw] + # if {$vmatch ne ""} { + # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values + # lappend pre_values [lpop remaining_rawargs 0] + # incr ridx + # continue + # } + #} + if {[llength $remaining_rawargs] < $min_clauselength} { #not enough remaining args to fill even this optional leader #rather than raise error here - perform our break (for end of leaders) and let the code below handle it break } - if {$valmin > 0 && [llength $remaining_rawargs] - $clauselength < $valmin} { + #check if enough remaining_rawargs to fill any required values + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { break } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values - #we still need to check if enough values for the leader itself - if {[llength $remaining_rawargs] < $clauselength} { - #not enough remaining args to fill *required* leader - break - } - incr ridx -1 - foreach v $leader_posn_name { - incr ridx - lappend pre_values [lpop remaining_rawargs 0] - } - if {!$is_multiple} { - incr nameidx - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break + #leadername may be a 'clause' of arbitrary length (e.g -type {int double} or {int string number}) + set end_leaders 0 + set tentative_pre_values [list] + set tentative_idx $ridx + if {$OPT_MAX ne "0"} { + foreach t $leader_type { + set raw [lindex $rawargs $tentative_idx] + if {[string match {\?*\?} $t] && [string match -* $raw]} { + #review - limitation of optional leaders is they can't be same value as any defined flags/opts + set flagname $raw + if {[string match --* $raw]} { + set eposn [string first = $raw] + # --flag=xxx + if {$eposn >=3} { + set flagname [string range $raw 0 $eposn-1] + } + } + set matchopt [::tcl::prefix::match -error {} $all_opts $flagname] + if {$matchopt ne ""} { + #don't consume if flaglike (and actually matches an opt) + set end_leaders 1 + break ;#break out of looking at -type members in the clause + } else { + #unrecognised flag - treat as value for optional member of the clause + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } + } else { + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } + } + if {$end_leaders} { + break + } } else { - if {$valmin > 0} { - if {[llength $remaining_rawargs] > $valmin} { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name + foreach t $leader_type { + #JJJ + set raw [lindex $rawargs $tentative_idx] + #lappend pre_values [lpop remaining_rawargs 0] + lappend tentative_pre_values $raw + incr tentative_idx + } + } + set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set consumed [dict get $assign_d consumed] + set resultlist [dict get $assign_d resultlist] + set newtypelist [dict get $assign_d typelist] + if {$consumed != 0} { + if {$leader_posn_name ni $leadernames_seen} { + lappend leadernames_seen $leader_posn_name + } + dict incr leader_posn_names_assigned $leader_posn_name + #for {set c 0} {$c < $consumed} {incr c} { + # lappend pre_values [lpop remaining_rawargs 0] + #} + lappend pre_values {*}[lrange $remaining_rawargs 0 $consumed-1] + ledit remaining_rawargs 0 $consumed-1 + + incr ridx $consumed + incr ridx -1 ;#leave ridx at index of last r that we set + } else { + + } + if {!$is_multiple} { + incr nameidx + } + } else { + #clause is required + if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { + #already accepted at least one complete clause for this name - requirement satisfied - now equivalent to optional + if {[llength $remaining_rawargs] < $min_clauselength} { + #not enough remaining args to fill even this optional leader + #rather than raise error here - perform our break (for end of leaders) and let the code below handle it + break + } + + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + break + } + } + #if we didn't break - requirement is not yet satisfied, or is satisfied but still enough remaining_rawargs for required values + #we still need to check if enough values for the leader itself + if {[llength $remaining_rawargs] < $min_clauselength} { + #not enough remaining args to fill *required* leader + break + } + + set end_leaders 0 + + #review - are we allowing multivalue leader clauses where the optional members are not at the tail? + #eg @leaders {double -type {?int? char}} + #as we don't type-check here while determining leaders vs opts vs values - this seems impractical. + #for consistency and simplification - we should only allow optional clause members at the tail + # and only for the last defined leader. This should be done in the definition parsing - not here. + foreach t $leader_type { + set raw [lindex $rawargs $ridx] + if {[string match {\?*\?} $t] && [string match -* $raw]} { + #review - limitation of optional leaders is they can't be same value as any defined flags/opts + + set matchopt [::tcl::prefix::match -error {} $all_opts $raw] + if {$matchopt ne ""} { + #don't consume if flaglike (and actually matches an opt) + set end_leaders 1 + break ;#break out of looking at -type members in the clause } else { - break + #unrecognised flag - treat as value for optional member of the clause + #ridx must be valid if we matched -* - so lpop will succeed + lappend pre_values [lpop remaining_rawargs 0] + incr ridx } } else { - lappend pre_values [lpop remaining_rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name + if {[string match {\?*\?} $t]} { + if {$valmin > 0 && [llength $remaining_rawargs] - $min_clauselength < $valmin} { + set end_leaders 1 + break + } + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break + } + } else { + if {[catch { + lappend pre_values [lpop remaining_rawargs 0] + }]} { + set end_leaders 1 + break ;#let validation of required leaders report the error? + } + } + incr ridx } } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop remaining_rawargs 0] + incr ridx -1 + if {$end_leaders} { + break + } + if {!$is_multiple} { + incr nameidx + } dict incr leader_posn_names_assigned $leader_posn_name } } else { - #review - if is_multiple, keep going if enough remaining_rawargs for values? - break + #unnamed leader + if {$LEADER_MIN ne "" } { + if {$ridx > $LEADER_MIN-1} { + if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { + break + } else { + if {$valmin > 0} { + if {[llength $remaining_rawargs] > $valmin} { + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } else { + break + } + } else { + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } + } + } else { + #haven't reached LEADER_MIN + lappend pre_values [lpop remaining_rawargs 0] + dict incr leader_posn_names_assigned $leader_posn_name + } + } else { + #review - if is_multiple, keep going if enough remaining_rawargs for values? + break + } } - } - #incr ridx - } ;# end foreach r $rawargs_copy + #incr ridx + } ;# end foreach r $rawargs_copy + } } + #puts "get_dict ================> pre: $pre_values" set argstate $ARG_INFO ;#argstate may have entries added set arg_checks $ARG_CHECKS @@ -4862,13 +7204,21 @@ tcl::namespace::eval punk::args { set leadermin $LEADER_MIN } if {$LEADER_MAX eq ""} { - set leadermax -1 + if {[llength $LEADER_NAMES] == 0 && !$LEADER_UNNAMED} { + set leadermax 0 + } else { + set leadermax -1 + } } else { set leadermax $LEADER_MAX } if {$VAL_MAX eq ""} { - set valmax -1 + if {[llength $VAL_NAMES] == 0 && !$VAL_UNNAMED} { + set valmax 0 + } else { + set valmax -1 + } } else { set valmax $VAL_MAX } @@ -4877,13 +7227,6 @@ tcl::namespace::eval punk::args { #assert - remaining_rawargs has been reduced by leading positionals set opts [dict create] ;#don't set to OPT_DEFAULTS here -#JJJ - #set id [dict get $argspecs id] - #if {$id eq "::if"} { - #puts stderr "::if" - #puts stderr "get_dict--> pre_values: $pre_values" - #puts stderr "get_dict--> remaining_rawargs: $remaining_rawargs" - #} set leaders [list] set arglist {} @@ -4892,7 +7235,7 @@ tcl::namespace::eval punk::args { #puts stderr "remaining_rawargs: $remaining_rawargs" #puts stderr "argstate: $argstate" if {$OPT_MAX ne "0" && [lsearch $remaining_rawargs -*] >= 0} { - #at least contains flaglike things.. + #contains at least one possible flag set maxidx [expr {[llength $remaining_rawargs] -1}] if {$valmax == -1} { set vals_total_possible [llength $remaining_rawargs] @@ -4902,7 +7245,6 @@ tcl::namespace::eval punk::args { set vals_remaining_possible $vals_total_possible } for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $remaining_rawargs $i] set remaining_args_including_this [expr {[llength $remaining_rawargs] - $i}] #lowest valmin is 0 if {$remaining_args_including_this <= $valmin} { @@ -4911,311 +7253,369 @@ tcl::namespace::eval punk::args { set post_values [lrange $remaining_rawargs $i end] break } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $remaining_rawargs 0 $i] - set post_values [lrange $remaining_rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted + set a [lindex $remaining_rawargs $i] + switch -glob -- $a { + -- { + if {$a in $OPT_NAMES} { #treat this as eopts - we don't care if remainder look like options or not lappend flagsreceived -- set arglist [lrange $remaining_rawargs 0 $i] set post_values [lrange $remaining_rawargs $i+1 end] + } else { + #assume it's a value. + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] } break - } else { - #a could be -short --longopt --longopt=val - + } + --* { set eposn [string first = $a] - if {[string match --* $a] && $eposn > 2} { + if {$eposn > 2} { #only allow longopt-style = for double leading dash longopts #--*= usage + if {$flagname ni $raw_optionset_members} { + # + set msg "Bad options for %caller%. Option $optionset at index [expr {$i-1}] requires a value, but '$flagname' not specified in definition to allow space-separated value." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list badoptionformat $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg + } + } + if {$solo_only} { + #same logic as 'solo' branch below for -type none + if {[tcl::dict::exists $argstate $optionset -typedefaults]} { + set tdflt [tcl::dict::get $argstate $optionset -typedefaults] } else { - #disallow "--longopt val" if only --longopt= was in optionset - #but we need to process "--longopt etc whatever..." as solo if 'optional' (?type?) - set solo_only false - if {[string match {\?*\?} $optionset_type]} { - #optional type - if {"$flagname=" ni $raw_optionset_members} { - set solo_only true - } else { - #--longopt= is present - if {"$flagname" ni $raw_optionset_members} { - #only parsing "--flag" or "--flag=val" is allowed by configuration -types ?type? - #we are in !$flagval_included branch so only solo left - # - set solo_only true - } - } + #normal default for a solo is 1 unless overridden by -typedefaults + set tdflt 1 + } + if {[tcl::dict::get $argstate $optionset -multiple]} { + if {$api_opt ni $flagsreceived} { + #override any default - don't lappend to it + tcl::dict::set opts $flag_ident $tdflt } else { - #flag value is non-optional - #no solo allowed - #--longopt= alone does not allow --longopt usage - if {$flagname ni $raw_optionset_members} { - # - set msg "Bad options for %caller%. Option $optionset at index [expr {$i-1}] requires a value, but '$flagname' not specified in definition to allow space-separated value." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list badoptionformat $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } + tcl::dict::lappend opts $flag_ident $tdflt } - if {$solo_only} { - #same logic as 'solo' branch below for -type none - if {[tcl::dict::get $argstate $optionset -multiple]} { - if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $api_opt 1 - } else { - tcl::dict::lappend opts $api_opt 1 - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - tcl::dict::set opts $api_opt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok + if {$api_opt ni $multisreceived} { + lappend multisreceived $api_opt + } + } else { + if {$flag_ident_is_parsekey} { + lappend opts $flag_ident $tdflt } else { - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - #review - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break - } - #flagval comes from next remaining rawarg - set flagval [lindex $remaining_rawargs $i+1] - if {[tcl::dict::get $argstate $optionset -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$api_opt ni $flagsreceived} { - tcl::dict::set opts $api_opt [list $flagval] - } else { - tcl::dict::lappend opts $api_opt $flagval - } - if {$api_opt ni $multisreceived} { - lappend multisreceived $api_opt - } - } else { - tcl::dict::set opts $api_opt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $optionset at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg - } + tcl::dict::set opts $flag_ident $tdflt } } + incr vals_remaining_possible -1 + lappend solosreceived $api_opt ;#dups ok } else { - #solo + #check if it was actually a value that looked like a flag + if {$i == $maxidx} { + #if no optvalue following - assume it's a value + #(caller should probably have used -- before it) + #review + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] + break + } + #flagval comes from next remaining rawarg + set flagval [lindex $remaining_rawargs $i+1] if {[tcl::dict::get $argstate $optionset -multiple]} { + #don't lappend to default - we need to replace if there is a default if {$api_opt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $api_opt 1 + tcl::dict::set opts $flag_ident [list $flagval] } else { - tcl::dict::lappend opts $api_opt 1 + tcl::dict::lappend opts $flag_ident $flagval } if {$api_opt ni $multisreceived} { lappend multisreceived $api_opt } } else { - tcl::dict::set opts $api_opt 1 + #tcl::dict::set opts $flag_ident $flagval + if {$flag_ident_is_parsekey} { + #necessary shimmer + lappend opts $flag_ident $flagval + } else { + tcl::dict::set opts $flag_ident $flagval + } } - incr vals_remaining_possible -1 - lappend solosreceived $api_opt ;#dups ok + #incr i to skip flagval + incr vals_remaining_possible -2 + if {[incr i] > $maxidx} { + set msg "Bad options for %caller%. No value supplied for last option $optionset at index [expr {$i-1}] which is not marked with -type none" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $optionset index [expr {$i-1}]] -badarg $optionset -argspecs $argspecs]] $msg + } + } + } + } else { + #none / solo + if {[tcl::dict::exists $argstate $optionset -typedefaults]} { + set tdflt [tcl::dict::get $argstate $optionset -typedefaults] + } else { + #normal default for a solo is 1 unless overridden by -typedefaults + set tdflt 1 + } + if {[tcl::dict::get $argstate $optionset -multiple]} { + if {$api_opt ni $flagsreceived} { + #override any default - don't lappend to it + tcl::dict::set opts $flag_ident $tdflt + } else { + tcl::dict::lappend opts $flag_ident $tdflt + } + if {$api_opt ni $multisreceived} { + lappend multisreceived $api_opt } - lappend flagsreceived $api_opt ;#dups ok } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break + #test parse_withdef_parsekey_repeat_ordering {Ensure last flag has precedence} + #tcl::dict::set opts $flag_ident $tdflt + if {$flag_ident_is_parsekey} { + #(shimmer - but required for ordering correctness during override) + lappend opts $flag_ident $tdflt + } else { + tcl::dict::set opts $flag_ident $tdflt + } + } + incr vals_remaining_possible -1 + lappend solosreceived $api_opt ;#dups ok + } + lappend flagsreceived $api_opt ;#dups ok + } else { + #starts with - but unmatched option flag + #comparison to valmin already done above + if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { + #todo - look at optspec_default and see if solo/vs opt-val pair + #we may need to lookahead by 2 regarding valmax valmin + + #even if optany - assume an unknown within the space of possible values is a value + #unmatched option in right position to be considered a value - treat like eopts + #review - document that an unspecified arg within range of possible values will act like eopts -- + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] + break + } + if {!([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { + if {$OPT_ANY} { + #exlude argument with whitespace from being a possible option e.g dict + #todo - passthrough of unrecognised --longopt=xxx without looking for following flag-value + set eposn [string first = $a] + if {[string match --* $a] && $eposn > 2} { + #only allow longopt-style = for double leading dash longopts + #--*= 2} { - #only allow longopt-style = for double leading dash longopts - #--*= $maxidx} { + set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg + #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a + } + incr vals_remaining_possible -2 } else { - set flagval [lindex $remaining_rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any|-arbitrary true - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $flagval - if {$a ni $multisreceived} { - lappend multisreceived $a - } + #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none + if {[tcl::dict::exists $argstate $a -typedefaults]} { + set tdflt [tcl::dict::get $argstate $a -typedefaults] + } else { + set tdflt 1 + } + if {[tcl::dict::get $argstate $a -multiple]} { + if {![tcl::dict::exists $opts $a]} { + tcl::dict::set opts $a $tdflt } else { - tcl::dict::set opts $a $flagval + tcl::dict::lappend opts $a $tdflt } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a + if {$a ni $multisreceived} { + lappend multisreceived $a } - incr vals_remaining_possible -2 } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a + tcl::dict::set opts $a $tdflt } + incr vals_remaining_possible -1 + lappend solosreceived $a } + } - lappend flagsreceived $flagreceived ;#adhoc flag name (if --x=1 -> --x) + lappend flagsreceived $undefined_flagsupplied ;#adhoc flag name (if --x=1 -> --x) + } else { + if {[llength $OPT_NAMES]} { + set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES (3)" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $optionset + set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any|-arbitrary false" } + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg + #arg_error $errmsg $argspecs -badarg $optionset } + } else { + #not a flag/option + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values [lrange $remaining_rawargs $i end] + break } - } else { - #not flaglike - set arglist [lrange $remaining_rawargs 0 $i-1] - set post_values [lrange $remaining_rawargs $i end] - break } + } #set values [list {*}$pre_values {*}$post_values] set leaders $pre_values @@ -5235,26 +7635,114 @@ tcl::namespace::eval punk::args { #} #--------------------------------------- + #Order the received options by the order in which they are *defined* + #EXCEPT that grouped options using same parsekey must be processed in received order set ordered_opts [dict create] - set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] - #unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) - # e.g -fg|-foreground - # e.g -x|--fullname= - #Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} - foreach o $unaliased_opts optset $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $optset]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $optset] + + #set unaliased_opts [lmap v $OPT_NAMES {string trimright [lindex [split $v |] end] =}] + ##unaliased_opts is list of 'api_opt' (to handle flag aliases of form -a1|-a2|-api_opt) + ## e.g -fg|-foreground + ## e.g -x|--fullname= + ##Resulting unaliased_opts from list {-fg|-foreground -x|--fullname=} should be {-foreground --fullname} + #foreach o $unaliased_opts optset $OPT_NAMES { + # if {[dict exists $opts $o]} { + # dict set ordered_opts $o [dict get $opts $o] + # } elseif {[dict exists $OPT_DEFAULTS $optset]} { + # #JJJ + # set parsekey "" + # if {[tcl::dict::exists $argstate $o -parsekey]} { + # set parsekey [tcl::dict::get $argstate $o -parsekey] + # } + # if {$parsekey eq ""} { + # set parsekey $o + # } + # dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + # } + #} + + #puts ">>>>==== $opts" + set seen_pks [list] + #treating opts as list for this loop. + foreach optset $OPT_NAMES { + set parsekey "" + set has_parsekey_override 0 + if {[tcl::dict::exists $argstate $optset -parsekey]} { + set parsekey [tcl::dict::get $argstate $optset -parsekey] + } + if {$parsekey eq ""} { + set has_parsekey_override 0 + #fall back to last element of aliased option e.g -fg|-foreground -> "-foreground" + set parsekey [string trimright [lindex [split $optset |] end] =] + } else { + set has_parsekey_override 1 + } + lappend seen_pks $parsekey + set is_found 0 + set foundkey "" + set foundval "" + #no lsearch -stride avail in 8.6 + foreach {k v} $opts { + if {$k eq $parsekey} { + set foundkey $k + set is_found 1 + set foundval $v + #can be multiple - last match wins - don't 'break' out of foreach + } + } ;#avoiding further dict/list shimmering + #if {[dict exists $opts $parsekey]} { + # set found $parsekey + # set foundval [dict get $opts $parsekey] + #} + if {!$is_found && $parsekey ne $optset} { + #.g we may have in opts things like: -decreasing|-SORTDIRECTION -increasing|-SORTDIRECTION + #(where -SORTDIRECTION was configured as -parsekey) + #last entry must win + #NOTE - do not use dict for here. opts is not strictly a dict - dupe keys will cause wrong ordering + foreach {o v} $opts { + if {[string match *|$parsekey $o]} { + set foundkey $o + set is_found 1 + set foundval $v + #last match wins - don't 'break' out of foreach + } + } + } + if {$is_found} { + dict set ordered_opts $foundkey $foundval + } elseif {[tcl::dict::exists $OPT_DEFAULTS $optset]} { + if {$parsekey ne $optset} { + set tailopt [string trimright [lindex [split $optset |] end] =] + if {$tailopt ne $parsekey} { + #defaults for multiple options sharing a -parsekey value ? review + dict set ordered_opts $tailopt|$parsekey [dict get $OPT_DEFAULTS $optset] + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + } + } else { + dict set ordered_opts $parsekey [dict get $OPT_DEFAULTS $optset] + } } } + #add in possible arbitrary opts after the defined opts, due to @opts directive flag '-any|-arbitrary true' - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval + #But make sure not to add any repeated parsekey e.g -increasing|-SORT -decreasing|-SORT + #use the seen_pks from the ordered_opts loop above + #keep working with opts only as list here.. + if {[llength $opts] > 2*[dict size $ordered_opts]} { + foreach {o o_val} $opts { + lassign [split $o |] _ parsekey ;#single pipe - 2 elements only | + if {$parsekey ne "" && $parsekey in $seen_pks} { + continue + } + if {![dict exists $ordered_opts $o]} { + dict set ordered_opts $o $o_val + } } } set opts $ordered_opts + #opts is a proper dict now + + #NOTE opts still may contain some entries in non-final form such as -flag|-PARSEKEY #--------------------------------------- @@ -5273,30 +7761,64 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + + set start_position $positionalidx set nameidx 0 #MAINTENANCE - (*nearly*?) same loop logic as for value for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} { - set leadername [lindex $LEADER_NAMES $nameidx] - #incr nameidx - set ldr [lindex $leaders $ldridx] + set leadername [lindex $LEADER_NAMES $nameidx] + set ldr [lindex $leaders $ldridx] if {$leadername ne ""} { - set typelist [tcl::dict::get $argstate $leadername -type] - if {[llength $typelist] == 1} { - set clauseval $ldr + set leadertypelist [tcl::dict::get $argstate $leadername -type] + set leader_clause_size [llength $leadertypelist] + + set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] + set consumed [dict get $assign_d consumed] + set resultlist [dict get $assign_d resultlist] + set newtypelist [dict get $assign_d typelist] + if {[tcl::dict::get $argstate $leadername -optional]} { + if {$consumed == 0} { + puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" + incr ldridx -1 + set leadername_multiple "" + incr nameidx + continue + } } else { - set clauseval [list] - incr ldridx -1 - foreach t $typelist { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername', but requires [llength $leadername] values" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadername] ] -argspecs $argspecs]] $msg + #required named arg + if {$consumed == 0} { + if {$leadername ni $leadernames_received} { + #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" + set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg + } else { + puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" + incr ldridx -1 + set leadername_multiple "" + incr nameidx + continue } - lappend clauseval [lindex $leaders $ldridx] } } + if {$leader_clause_size == 1} { + #set clauseval $ldr + set clauseval [lindex $resultlist 0] + } else { + set clauseval $resultlist + incr ldridx [expr {$consumed - 1}] + + #not quite right.. this sets the -type for all clauses - but they should run independently + #e.g if expr {} elseif 2 {script2} elseif 3 then {script3} (where elseif clause defined as "literal(elseif) expr ?literal(then)? script") + #the elseif 2 {script2} will raise an error because the newtypelist from elseif 3 then {script3} overwrote the newtypelist where then was given the type ?omitted-...? + + tcl::dict::set argstate $leadername -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries + } + if {[tcl::dict::get $argstate $leadername -multiple]} { #if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { # #current stored ldr equals defined default - don't include default in the list we build up @@ -5306,29 +7828,32 @@ tcl::namespace::eval punk::args { #} if {$leadername in $leadernames_received} { tcl::dict::lappend leaders_dict $leadername $clauseval + tcl::dict::lappend argument_clause_typestate $leadername $newtypelist } else { tcl::dict::set leaders_dict $leadername [list $clauseval] + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] } set leadername_multiple $leadername } else { tcl::dict::set leaders_dict $leadername $clauseval + tcl::dict::set argument_clause_typestate $leadername [list $newtypelist] set leadername_multiple "" incr nameidx } lappend leadernames_received $leadername } else { if {$leadername_multiple ne ""} { - set typelist [tcl::dict::get $argstate $leadername_multiple -type] - if {[llength $typelist] == 1} { + set leadertypelist [tcl::dict::get $argstate $leadername_multiple -type] + if {[llength $leadertypelist] == 1} { set clauseval $ldr } else { set clauseval [list] incr ldridx -1 - foreach t $typelist { + foreach t $leadertypelist { incr ldridx if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires [llength $leadername_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadername_multiple] ] -argspecs $argspecs]] $msg + set msg "Bad number of leaders for %caller%. Received [llength $clauseval] values ('$clauseval') for '$leadername_multiple', but requires up to [llength $leadertypelist] values." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $leadertypelist] ] -argspecs $argspecs]] $msg } lappend clauseval [lindex $leaders $ldridx] } @@ -5352,7 +7877,19 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- #satisfy test parse_withdef_leaders_no_phantom_default - foreach leadername [dict keys $leaders_dict] { + #foreach leadername [dict keys $leaders_dict] { + # if {[string is integer -strict $leadername]} { + # #ignore leadername that is a positionalidx + # #review - always trailing - could use break? + # continue + # } + # if {$leadername ni $leadernames_received && ![dict exists $LEADER_DEFAULTS $leadername]} { + # #remove the name with empty-string default we used to establish fixed order of names + # #The 'leaders' key in the final result shouldn't contain an entry for an argument that wasn't received and had no default. + # dict unset leaders_dict $leadername + # } + #} + dict for {leadername _v} $leaders_dict { if {[string is integer -strict $leadername]} { #ignore leadername that is a positionalidx #review - always trailing - could use break? @@ -5366,6 +7903,7 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- + set validx 0 set valname_multiple "" set valnames_received [list] @@ -5388,15 +7926,19 @@ tcl::namespace::eval punk::args { #MAINTENANCE - (*nearly*?) same loop logic as for leaders for {set validx 0} {$validx < [llength $values]} {incr validx} { set valname [lindex $VAL_NAMES $nameidx] - set val [lindex $values $validx] + set val [lindex $values $validx] if {$valname ne ""} { set valtypelist [tcl::dict::get $argstate $valname -type] + set clause_size [llength $valtypelist] ;#common case is clause_size == 1 set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] + set newtypelist [dict get $assign_d typelist] if {[tcl::dict::get $argstate $valname -optional]} { if {$consumed == 0} { + #error 333 + puts stderr "get_dict cannot assign val:$val to valname:$valname (333)" incr validx -1 set valname_multiple "" incr nameidx @@ -5410,6 +7952,8 @@ tcl::namespace::eval punk::args { set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { + #error 444 + puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" incr validx -1 set valname_multiple "" incr nameidx @@ -5419,30 +7963,24 @@ tcl::namespace::eval punk::args { } #assert can_assign != 0, we have at least one value to assign to clause - if {[llength $valtypelist] == 1} { - set clauseval $val + if {$clause_size == 1} { + #set clauseval $val + set clauseval [lindex $resultlist 0] } else { #clauseval must contain as many elements as the max length of -types! #(empty-string/default for optional (?xxx?) clause members) set clauseval $resultlist #_get_dict_can_assign has only validated clause-length and literals match #we assign and leave further validation for main validation loop. - incr validx -1 - incr validx $consumed + incr validx [expr {$consumed -1}] if {$validx > [llength $values]-1} { error "get_dict unreachable" - set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valname] ] -argspecs $argspecs]] $msg + set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to $clause_size values." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength $clause_size ] -argspecs $argspecs]] $msg } - #for {set i 0} {$i < $consumed} {incr i} { - # incr validx - # if {$validx > [llength $values]-1} { - # set msg "Bad number of values for %caller%. Received [llength $clauseval] values for clause '$valname', but requires up to [llength $valtypelist] values." - # return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list clausevaluelength [llength $clauseval] clauselength [llength $valname] ] -argspecs $argspecs]] $msg - # } - # #lappend clauseval [lindex $values $validx] - #} + #incorrect - we shouldn't update the default. see argument_clause_typestate dict of lists of -type + tcl::dict::set argstate $valname -type $newtypelist ;#(possible ?omitted-? and ?defaulted-? entries } if {[tcl::dict::get $argstate $valname -multiple]} { @@ -5454,17 +7992,21 @@ tcl::namespace::eval punk::args { #} if {$valname in $valnames_received} { tcl::dict::lappend values_dict $valname $clauseval + tcl::dict::lappend argument_clause_typestate $valname $newtypelist } else { tcl::dict::set values_dict $valname [list $clauseval] + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] } set valname_multiple $valname } else { tcl::dict::set values_dict $valname $clauseval + tcl::dict::set argument_clause_typestate $valname [list $newtypelist] ;#list protect set valname_multiple "" incr nameidx } lappend valnames_received $valname } else { + #unnamed if {$valname_multiple ne ""} { set valtypelist [tcl::dict::get $argstate $valname_multiple -type] if {[llength $valname_multiple] == 1} { @@ -5513,6 +8055,10 @@ tcl::namespace::eval punk::args { } } #----------------------------------------------------- + #JJJJJJ + #if {[dict size $argument_clause_typestate]} { + # puts ">>>>>[dict get $argspecs id] typestate $argument_clause_typestate" + #} if {$leadermax == -1} { #only check min @@ -5551,6 +8097,7 @@ tcl::namespace::eval punk::args { } #assertion - opts keys are full-length option names if -any|-arbitrary was false or if the supplied option as a shortname matched one of our defined options + #(and may still contain non-final flag_ident entries such as -increasing|-SORTDIRECTION) #opts explicitly marked as -optional 0 must be present - regardless of -any|-arbitrary (which allows us to ignore additional opts to pass on to next call) @@ -5571,22 +8118,28 @@ tcl::namespace::eval punk::args { # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" #} #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + if {[llength $LEADER_REQUIRED]} { + if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { + set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg + #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + } } - set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] - if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { - set full_missing [dict get $lookup_optset $missing] - set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs + if {[llength $OPT_REQUIRED]} { + set api_opt_required [lmap v $OPT_REQUIRED {lindex [split $v |] end}] + if {[llength [set missing [punklib_ldiff $api_opt_required $flagsreceived]]]} { + set full_missing [dict get $lookup_optset $missing] + set msg "Required option missing for %caller%. missing flags: '$full_missing' are marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $full_missing received $flagsreceived] -argspecs $argspecs]] $msg + #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs + } } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + if {[llength $VAL_REQUIRED]} { + if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { + set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg + #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs + } } #--------------------------------------------------------------------------------------------- @@ -5614,20 +8167,58 @@ tcl::namespace::eval punk::args { #check types,ranges,choices set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" + #puts "get_dict>>>>>>>> ---opts_and_values:$opts_and_values" + #puts " >>>>>>> ---lookup_optset :$lookup_optset" #puts "---argstate:$argstate" - tcl::dict::for {api_argname value_group} $opts_and_values { - if {[string match -* $api_argname]} { - #get full option name such as -fg|-foreground from non-alias name such as -foreground - #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined - if {[dict exists $lookup_optset $api_argname]} { - set argname [dict get $lookup_optset $api_argname] + #JJJ argname_or_ident; ident example: -increasing|-SORTOPTION + tcl::dict::for {argname_or_ident value_group} $opts_and_values { + # + #parsekey: key used in resulting leaders opts values dictionaries + # often distinct from the full argname in the ARG_INFO structure + # + if {[string match -* $argname_or_ident]} { + #ident format only applies to options/flags + if {[string first | $argname_or_ident] > -1} { + #flag_ident style (grouped fullname of option with -parsekey) + lassign [split $argname_or_ident |] fullflag parsekey ;#we expect only a single pipe in ident form | + if {[dict exists $lookup_optset $fullflag]} { + set argname [dict get $lookup_optset $fullflag] + #idents should already have correct parsekey + } else { + puts stderr "punk::args::get_dict unable to find $fullflag in $lookup_optset (parsekey:$parsekey) (value_group: $value_group)" + } } else { - puts stderr "unable to find $api_argname in $lookup_optset" + if {[dict exists $lookup_optset $argname_or_ident]} { + #get full option name such as -fg|-foreground from non-alias name such as -foreground + #if "@opts -any|-arbitrary true" - we may have an option that wasn't defined + set argname [dict get $lookup_optset $argname_or_ident] + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #default parsekey: last element in argname without trailing = + set parsekey [string trimright [lindex [split $argname |] end] =] + } + } else { + puts stderr "punk::args::get_dict unable to find $argname_or_ident in $lookup_optset (value_group: $value_group)" + } } } else { - set argname $api_argname + set argname $argname_or_ident + set pkoverride [Dict_getdef $argstate -parsekey ""] + if {$pkoverride ne ""} { + set parsekey $pkoverride + } else { + #leader or value of form x|y has no special meaning and forms the parsekey in entirety by default. + set parsekey $argname + } } + #assert: argname is the key for the relevant argument info in the FORMS//ARG_INFO dict. (here each member available as $argstate) + #argname is usually the full name as specified in the definition: + #e.g -f|-path|--filename= + # (where the parsekey will be by default --filename, possibly overridden by -parsekey value) + #an example argname_or_compound for the above might be: -path|--filename + # where -path is the expanded form of the actual flag used (could have been for example just -p) and --filename is the parsekey set thisarg [tcl::dict::get $argstate $argname] #set thisarg_keys [tcl::dict::keys $thisarg] @@ -5641,21 +8232,55 @@ tcl::namespace::eval punk::args { set defaultval [tcl::dict::get $thisarg -default] } set typelist [tcl::dict::get $thisarg -type] + set clause_size [llength $typelist] set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 set validationtransform [tcl::dict::get $thisarg -validationtransform] + #JJJJ + #if {$is_multiple} { + # set vlist $value_group + #} else { + # set vlist [list $value_group] + #} + ##JJJJ + #if {$clause_size == 1} { + # set vlist [list $vlist] + #} + + + #JJ 2025-07-25 + set vlist [list] + #vlist is a list of clauses. Each clause is a list of values of length $clause_size. + #The common case is clause_size 1 - but as we need to treat each clause as a list during validation - we need to list protect the clause when clause_size == 1. if {$is_multiple} { - set vlist $value_group + if {$clause_size == 1} { + foreach c $value_group { + lappend vlist [list $c] + } + } else { + set vlist $value_group + } } else { - set vlist [list $value_group] + if {$clause_size ==1} { + set vlist [list [list $value_group]] + } else { + set vlist [list $value_group] + } } - #JJJJ - if {[llength $typelist] == 1} { - set vlist [list $vlist] + set vlist_typelist [list] + if {[dict exists $argument_clause_typestate $argname]} { + #lookup saved newtypelist (argument_clause_typelist) from can_assign_value result where some optionals were given type ?omitted-? or ?defaulted-? + # args.test: parse_withdef_value_clause_missing_optional_multiple + set vlist_typelist [dict get $argument_clause_typestate $argname] + } else { + foreach v $vlist { + lappend vlist_typelist $typelist + } } + + + set vlist_original $vlist ;#retain for possible final strip_ansi #review - validationtransform @@ -5664,7 +8289,12 @@ tcl::namespace::eval punk::args { package require punk::ansi set vlist_check [list] foreach clause_value $vlist { - lappend vlist_check [punk::ansi::ansistrip $clause_value] + #lappend vlist_check [punk::ansi::ansistrip $clause_value] + set stripped [list] + foreach element $clause_value { + lappend stripped [punk::ansi::ansistrip $element] + } + lappend vlist_check $stripped } } else { #validate_ansistripped 0 @@ -5689,9 +8319,12 @@ tcl::namespace::eval punk::args { set argclass "Unknown argument" } } + set vlist_validate [list] + set vlist_check_validate [list] + set vlist_typelist_validate [list] #reduce our validation requirements by removing values which match defaultval or match -choices #(could be -multiple with -choicerestricted 0 where some selections match and others don't) - if {$api_argname in $receivednames && $has_choices} { + if {$parsekey in $receivednames && $has_choices} { #-choices must also work with -multiple #todo -choicelabels set choiceprefix [tcl::dict::get $thisarg -choiceprefix] @@ -5716,600 +8349,335 @@ tcl::namespace::eval punk::args { #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - set idx 0 ;# + set clause_index -1 ;# #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes #assert llength $vlist == llength [dict get $dname $argname] # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + foreach clause $vlist clause_check $vlist_check clause_typelist $vlist_typelist { + incr clause_index + set element_index -1 ;#element within clause - usually clause size is only 1 + foreach e $clause e_check $clause_check { + incr element_index + set allchoices_in_list 0 + if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { + #vlist and vlist_check can be list of lists if -multiple and -choicemultiple + #each e represents 0 or more choice selections + set c_list $e + set c_check_list $e_check + #todo? check if entire list matches default? } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] - if {$chosen eq "" || $chosen in $choiceprefixreservelist} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #todo - don't add to validation lists if not in receivednames - #if we have an optionset such as "-f|-x|-etc" api_argname is -etc - if {$api_argname ni $receivednames} { - set vlist [list] - set vlist_check_validate [list] - } else { - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach clause_value $vlist { - foreach e $clause_value { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg + #only one choice at a time - ensure single entry in c_list c_check_list + set c_list [list $e] + set c_check_list [list $e_check] } - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - for {set t 0} {$t < [llength $typelist]} {incr t} { - set typespec [lindex $typelist $t] - set type [string trim $typespec ?] - puts "$argname - switch on type: $type" - switch -- $type { - any {} - literal { - foreach clause_value $vlist { - set e [lindex $clause_value $t] - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - list { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } + #----------------------------------- + #fast fail on the wrong number of choices + if {[llength $c_list] < $choicemultiple_min} { + set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg } - indexexpression { - foreach clause_value $vlist_check { - set e_check [lindex $clause_value $t] - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } + if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { + set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - if {[regexp [lindex $regexprepass $t] $e]} { - lappend pass_quick_list_e $clauseval - lappend pass_quick_list_e_check $clauseval_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach clauseval $remaining_e clauseval_check $remaining_e_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach clauseval $remaining_e { - set e [lindex $clauseval $t] - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } + #----------------------------------- - if {[tcl::dict::size $thisarg_checks]} { - foreach clauseval $remaining_e_check { - set e_check [lindex $clauseval $t] - if {[dict exists $thisarg_checks -minsize]} { - set minsize [dict get $thisarg_checks -minsize] - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $minsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } + set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list + foreach c $c_list c_check $c_check_list { + if {$nocase} { + set casemsg " (case insensitive)" + set choices_test [tcl::string::tolower $allchoices] + #Don't lcase the denylist - even in nocase mode! + #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] + set v_test [tcl::string::tolower $c_check] + } else { + set casemsg " (case sensitive)" + set choices_test $allchoices + set v_test $c_check } - if {[tcl::dict::exists $thisarg -range]} { - set ranges [tcl::dict::get $thisarg -range] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign {} low high ;#set both empty - lassign $range low high - - if {"$low$high" ne ""} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + set choice_in_list 0 + set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? + if {!$matches_default} { + if {$choiceprefix} { + #can we handle empty string as a choice? It should just work - REVIEW/test + set choice_exact_match 0 + if {$c_check in $allchoices} { + #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing + set chosen $c_check + set choice_in_list 1 + set choice_exact_match 1 + } elseif {$v_test in $choices_test} { + #assert - if we're here, nocase must be true + #we know choice is present as full-length match except for case + #now we want to select the case from the choice list - not the supplied value + #we don't set choice_exact_match - because we will need to override the optimistic existing val below + #review + foreach avail [lsort -unique $allchoices] { + if {[string match -nocase $c $avail]} { + set chosen $avail } } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::exists $thisarg -range]} { - set ranges [tcl::dict::get $thisarg -range] - foreach clauseval $vlist clauseval_check $vlist_check { - set e [lindex $clauseval $t] - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - lassign $range low high - if {"$low$high" ne ""} { - if {$low eq ""} { - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg + #assert chosen will always get set + set choice_in_list 1 + } else { + #puts ">>>> choiceprefixreservelist: $choiceprefixreservelist" + #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. + #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. + #in this block we can treat empty result from prefix match as a non-match + if {$nocase} { + #nocase implies that our entered value doesn't have to match case of choices - + #but we would still like to select the best match if there are case-dups. + #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete + # selecting Del will find Delete, del will match delete (and raise error) + # but DEL will also match delete rather than Delete - so again an error is raised. + #This is counterintuitive with -nocase + #This is probably such an edge case that best served with documentation as a feature-not-bug + #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? + #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. + + set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} { + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test] + #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing + set chosen [lsearch -inline -nocase $allchoices $chosen] + set choice_in_list [expr {$chosen ne ""}] + } else { + set chosen $bestmatch + set choice_in_list 1 } } else { - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - double { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is double -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -range]} { - set ranges [dict get $thisarg_checks -range] - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set range [lindex $ranges $t] - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $range low high - if {$e_check < $low || $e_check > $high} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - bool { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - if {[tcl::dict::size $thisarg_checks]} { - if {[dict exists $thisarg_checks -minsize]} { - set minsizes [dict get $thisarg_checks -minsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set minsize [lindex $minsizes $t] - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $minsize} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $minsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $minsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check] + if {$chosen eq "" || $chosen in $choiceprefixreservelist} { + set choice_in_list 0 + } else { + set choice_in_list 1 + } + } + #override choice_in_list if in deny list + #don't allow prefixing for elements from -choiceprefixdenylist + #we still use all elements to calculate the prefixes though + #review - case difference edge cases in choiceprefixdenylist !todo + if {$chosen in $choiceprefixdenylist} { + set choice_in_list 0 + set chosen "" } } - } - if {[dict exists $thisarg_checks -maxsize]} { - set maxsizes [dict get $thisarg_checks -maxsize] - foreach clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - set maxsize [lindex $maxsizes $t] - if {$maxsize ne "-1"} { - if {[tcl::dict::size $e_check] > $maxsize} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $maxsize. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $maxsize] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg + + #override the optimistic existing val + #our existing values in $dname are not list-protected - so we need to check clause_size + if {$choice_in_list && !$choice_exact_match} { + set existing [tcl::dict::get [set $dname] $argname_or_ident] + if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { + #single choice allowed per clause-member + if {$is_multiple} { + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $element_index $chosen + } else { + lset existing $clause_index $element_index $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + #test: choice_multielement_clause + lset existing $element_index $chosen + tcl::dict::set $dname $argname_or_ident $existing + } + } else { + if {$is_multiple} { + #puts ">>> existing $existing $choice_idx" + if {$clause_size == 1} { + #no list wrapping of single element in $dname dict - so don't index into it with element_index + lset existing $clause_index $choice_idx $chosen + } else { + lset existing $clause_index $element_index $choice_idx $chosen + } + tcl::dict::set $dname $argname_or_ident $existing + } else { + lset existing $element_index $choice_idx $chosen + tcl::dict::set $dname $argname_or_ident $existing } } } + } else { + #value as stored in $dname is ok + set choice_in_list [expr {$v_test in $choices_test}] } } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {![tcl::string::is $type -strict $e_check]} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg + + if {!$choice_in_list && !$matches_default} { + if {!$choicerestricted} { + #if {$is_multiple} { + # set existing [tcl::dict::get [set $dname] $argname] + # lset existing $clause_index $v_test + # tcl::dict::set $dname $argname $existing + #} else { + # tcl::dict::set $dname $argname $v_test + #} + #JJJ + #lappend vlist_validate $c + #lappend vlist_check_validate $c_check + } else { + #unhappy path + if {$choiceprefix} { + set prefixmsg " (or a unique prefix of a value)" + } else { + set prefixmsg "" + } + #review: $c vs $c_check for -badval? + set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg + #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname } + } else { + #choice is in list or matches default - no validation for this specific element in the clause + lset clause_typelist $element_index any } + incr choice_idx } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } + + } ;#end foreach e in clause + #jjj 2025-07-16 + #if not all clause_typelist are 'any' + if {[lsearch -not $clause_typelist any] > -1} { + #at least one element still needs validation + lappend vlist_validate $clause + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist + } + + + } ;#end foreach clause in vlist + + #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation + #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups + set vlist $vlist_validate + set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate + } + + #todo - don't add to validation lists if not in receivednames + #if we have an optionset such as "-f|-x|-etc"; the parsekey is -etc (unless it was overridden by -parsekey in definition) + if {$parsekey ni $receivednames} { + set vlist [list] + set vlist_check_validate [list] + } else { + if {[llength $vlist] && $has_default} { + #defaultval here is a value for the entire clause. (clause usually length 1) + #J2 + #set vlist_validate [list] + #set vlist_check_validate [list] + set tp [dict get $thisarg -type] + set clause_size [llength $tp] + foreach clause_value $vlist clause_check $vlist_check clause_typelist $vlist_typelist { + #JJJJ + #REVIEW!!! we're inadvertently adding back in things that may have already been decided in choicelist loop as not requiring validation? + if {$clause_value ni $vlist_validate} { + if {$clause_size ==1} { + #for -choicemultiple with default that could be a list use 'ni' + #?? review! + if {[lindex $clause_check 0] ne $defaultval} { + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } + } else { + if {$clause_check ne $defaultval} { + lappend vlist_validate $clause_value + lappend vlist_check_validate $clause_check + lappend vlist_typelist_validate $clause_typelist } } } - char { - #review - char vs unicode codepoint vs grapheme? - foreach clauseval $vlist clauseval_check $vlist_check { - set e_check [lindex $clauseval_check $t] - if {[tcl::string::length $e_check] != 1} { - set e [lindex $clauseval $t] - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } + #if {[llength $tp] == 1} { + # if {$clause_value ni $vlist_validate} { + # #for -choicemultiple with default that could be a list use 'ni' + # #?? review! + # if {[lindex $clause_check 0] ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} else { + # if {$clause_value ni $vlist_validate} { + # if {$clause_check ne $defaultval} { + # lappend vlist_validate $clause_value + # lappend vlist_check_validate $clause_check + # } + # } + #} + #Todo? + #else ??? + } + set vlist $vlist_validate + set vlist_check $vlist_check_validate + set vlist_typelist $vlist_typelist_validate + } + } + + #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups + #assert: our vlist & vlist_check lists have been reduced to remove those + if {[llength $vlist] && !$is_allow_ansi} { + #allow_ansi 0 + package require punk::ansi + #do not run ta::detect on a list + foreach clause_value $vlist { + foreach e $clause_value { + if {[punk::ansi::ta::detect $e]} { + set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg + } + } + } + } + #puts "argname:$argname v:$v is_default:$is_default" + #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value + #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. + #arguments that are at their default are not subject to type and other checks + + #don't validate defaults or choices that matched + #puts "---> opts_and_values: $opts_and_values" + #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" + #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} + + #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups + #assert [llength $vlist] == [llength $vlist_check] + if {[llength $vlist]} { + #$t = clause column + + #for {set clausecolumn 0} {$clausecolumn < [llength $typelist]} {incr clausecolumn} {} + set clausecolumn -1 + foreach typespec $typelist { + incr clausecolumn + if {[dict exists $thisarg -typedefaults]} { + set tds [dict get $thisarg -typedefaults] + if {[lindex $vlist $clausecolumn] eq [lindex $tds $clausecolumn]} { + continue } } + set type_expression [string trim $typespec ?] + if {$type_expression in {any none}} { + continue + } + #puts "$argname - switch on type_expression: $type_expression v:[lindex $vlist $clausecolumn]" + #set typespec [lindex $typelist $clausecolumn] + #todo - handle type-alternates e.g -type char|double + #------------------------------------------------------------------------------------ + #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #------------------------------------------------------------------------------------ + + + #todo - pass validation if matches an entry in -typedefaults + #has_typedefault? + #set typedefault [lindex $typedefaults $clausecolumn] + + } @@ -6320,37 +8688,57 @@ tcl::namespace::eval punk::args { if {[tcl::dict::get $thisarg -multiple]} { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict $argname $stripped_list + tcl::dict::set leaders_dict $argname_or_ident $stripped_list } option { - tcl::dict::set opts $argname $stripped_list + tcl::dict::set opts $argname_or_ident $stripped_list } value { - tcl::dict::set values_dict $argname $stripped_list + tcl::dict::set values_dict $argname_or_ident $stripped_list } } } else { switch -- [tcl::dict::get $thisarg -ARGTYPE] { leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] + tcl::dict::set leaders_dict $argname_or_ident [lindex $stripped_list 0] } option { - tcl::dict::set opts $argname [lindex $stripped_list 0] + tcl::dict::set opts $argname_or_ident [lindex $stripped_list 0] } value { - tcl::dict::set values_dict [lindex $stripped_list 0] + tcl::dict::set values_dict $argname_or_ident [lindex $stripped_list 0] } } } } } - - - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] + set finalopts [dict create] + dict for {o v} $opts { + if {[string first | $o] > -1} { + #set parsekey [lindex [split $o |] end] + dict set finalopts [lindex [split $o |] end] $v + } else { + dict set finalopts $o $v + } + } + return [tcl::dict::create leaders $leaders_dict opts $finalopts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] } - + lappend PUNKARGS [list { + @id -id ::punk::args::forms + @cmd -name punk::args::forms\ + -summary\ + "List command forms."\ + -help\ + "Return names for each form of a command identified by 'id'. + Most commands are single-form and will only return the name '_default'." + @leaders -min 0 -max 0 + @opts + @values -min 1 -max 1 + id -multiple 0 -optional 0 -help\ + "Exact id of command" + }] proc forms {id} { set spec [get_spec $id] if {[dict size $spec]} { @@ -6359,15 +8747,46 @@ tcl::namespace::eval punk::args { return [list] } } + + + lappend PUNKARGS [list { + @id -id ::punk::args::eg + @cmd -name punk::args::eg\ + -summary\ + "Command examples."\ + -help\ + "Return command examples from -help in @examples + directive of a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + }] + proc eg {args} { + set argd [punk::args::parse $args withid ::punk::args::eg] + lassign [dict values $argd] leaders opts values received + set cmditems [dict get $values cmditem] + set id [lindex $cmditems 0] + set cmdargs [lrange $cmditems 1 end] + + set spec [get_spec $id] + if {$spec eq ""} { + return + } + return [dict get $spec examples_info -help] + } + lappend PUNKARGS [list { @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id + @cmd -name punk::args::synopsis\ + -summary\ + "Command synopsis"\ + -help\ + "Return synopsis for each form of a command on separate lines. If -form is given, supply only the synopsis for that form. " @opts + -noheader -type none -form -type string -default * -return -type string -default full -choices {full summary dict} @values -min 1 -max -1 @@ -6382,10 +8801,18 @@ tcl::namespace::eval punk::args { set has_punkansi 1 } if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] + set I [punk::ansi::a+ italic] + set NI [punk::ansi::a+ noitalic] + #for inner question marks marking optional type + set IS [punk::ansi::a+ italic strike] + set NIS [punk::ansi::a+ noitalic nostrike] + #set RST [punk::ansi::a] + set RST "\x1b\[m" } else { set I "" + set NI "" + set IS "" + set NIS "" set RST "" } @@ -6402,12 +8829,12 @@ tcl::namespace::eval punk::args { ##set id [lindex $arglist 0] ##set cmdargs [lrange $arglist 1 end] - lassign [dict values $argd] leaders opts values + lassign [dict values $argd] leaders opts values received set form [dict get $opts -form] set opt_return [dict get $opts -return] set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] + set id [lindex $cmditems 0] + set cmdargs [lrange $cmditems 1 end] set spec [get_spec $id] @@ -6433,90 +8860,346 @@ tcl::namespace::eval punk::args { } set SYND [dict create] - set syn "" + dict set SYND cmd_info [dict get $spec cmd_info] + #leading "# " required (punk::ns::synopsis will pass through) + if {![dict exists $received -noheader]} { + set syn "# [Dict_getdef $spec cmd_info -summary ""]\n" + } #todo - -multiple etc foreach f $form_names { set SYNLIST [list] - dict set SYND $f [list] + dict set SYND FORMS $f [list] append syn "$id" set forminfo [dict get $spec FORMS $f] + #foreach argname [dict get $forminfo LEADER_NAMES] { + # set arginfo [dict get $forminfo ARG_INFO $argname] + # set ARGD [dict create argname $argname class leader] + # if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { + # set display [lindex [dict get $arginfo -choices] 0] + # } elseif {[dict get $arginfo -type] eq "literal"} { + # set display $argname + # } else { + # set display $I$argname$RST + # } + # if {[dict get $arginfo -optional]} { + # append syn " ?$display?" + # } else { + # append syn " $display" + # } + # dict set ARGD type [dict get $arginfo -type] + # dict set ARGD optional [dict get $arginfo -optional] + # dict set ARGD display $display + # dict lappend SYND $f $ARGD + #} + set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname + set arginfo [dict get $forminfo ARG_INFO $argname] + set typelist [dict get $arginfo -type] + if {[llength $typelist] == 1} { + set tp [lindex $typelist 0] + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + #set arg_display [dict get $arginfo -typesynopsis] + set clause $ts + } else { + #set arg_display $argname + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + set type_alternatives [_split_type_expression $tp] + foreach tp_alternative $type_alternatives { + set firstword [lindex $tp_alternative 0] + switch -exact -- $firstword { + literal { + set match [lindex $tp_alternative 1] + lappend alternates $match + } + literalprefix { + #todo - trie styling on prefix calc + set match [lindex $tp_alternative 1] + lappend alternates $match + } + stringstartswith { + set match [lindex $tp_alternative 1] + lappend alternates $match* + } + stringendswith { + set match [lindex $tp_alternative 1] + lappend alternates *$match + } + default { + lappend alternates $I$argname$NI + } + } + + #if {$tp_alternative eq "literal"} { + # lappend alternates [lindex $argname end] + #} elseif {[string match literal(*) $tp_alternative]} { + # set match [string range $tp_alternative 8 end-1] + # lappend alternates $match + #} elseif {[string match literalprefix(*) $tp_alternative]} { + # set match [string range $tp_alternative 14 end-1] + # lappend alternates $match + #} else { + # lappend alternates $I$argname$NI + #} + } + #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) + #todo - trie prefixes display + set alternates [punk::args::lib::lunique $alternates] + set clause [join $alternates |] + } } else { - set display $I$argname$RST + set n [expr {[llength $typelist]-1}] + set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types + set clause "" + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts + } else { + set tp_displaylist [lrepeat [llength $typelist] ""] + } + + foreach typespec $typelist td $tp_displaylist elementname $name_tail { + #elementname will commonly be empty + if {[string match {\?*\?} $typespec]} { + set tp [string range $typespec 1 end-1] + set member_optional 1 + } else { + set tp $typespec + set member_optional 0 + } + if {$tp eq "literal"} { + set c $elementname + } elseif {[string match literal(*) $tp]} { + set match [string range $tp 8 end-1] + set c $match + } else { + if {$td eq ""} { + set c $I$tp$NI + } else { + set c $td + } + } + if {$member_optional} { + append clause " " "(?$c?)" + } else { + append clause " " $c + } + } + set clause [string trimleft $clause] } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" + + set ARGD [dict create argname $argname class leader] + if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { + if {[dict get $arginfo -multiple]} { + #set display "?$I$argname$NI?..." + set display "?$clause?..." + } else { + set display "?$clause?" + #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { + # set display "?[lindex [dict get $arginfo -choices] 0]?" + #} elseif {[dict get $arginfo -type] eq "literal"} { + # set display "?$argname?" + #} else { + # set display "?$I$argname$NI?" + #} + } } else { - append syn " $display" + if {[dict get $arginfo -multiple]} { + #set display "$I$argname$NI ?$I$argname$NI?..." + set display "$clause ?$clause?..." + } else { + set display $clause + #if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { + # set display "[lindex [dict get $arginfo -choices] 0]" + #} elseif {[dict get $arginfo -type] eq "literal"} { + # set display $argname + #} else { + # set display "$I$argname$NI" + #} + } } + append syn " $display" dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } foreach argname [dict get $forminfo OPT_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] set ARGD [dict create argname $argname class option] set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } + if {$tp eq "none"} { + #assert - argname may have aliases delimited by | - but no aliases end with = + #(disallowed in punk::args::define) + set argdisplay $argname + } else { + #assert [llength $tp] == 1 (multiple values for flag unspported in punk::args::define) + if {[string match {\?*\?} $tp]} { + set tp [string range $tp 1 end-1] + set value_is_optional true + } else { + set value_is_optional false + } + + + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_display $ts + #user may or may not have remembered to match the typesynopsis with the optionality by wrapping with ? + #review - if user wrapped with ?*? and also leading/trailing ANSI - we won't properly strip + #todo - enforce no wrapping '?*?' in define for -typesynopsis? + set tp_display [string trim $tp_display ?] } else { - if {$tp eq "none"} { - set display "?$argname?" + + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + foreach tp_alternative [split $tp |] { + #-type literal not valid for opt - review + if {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] + lappend alternates $match + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] + lappend alternates $match + } else { + lappend alternates <$I$tp_alternative$NI> + } + } + #todo - trie prefixes display? + set alternates [punk::args::lib::lunique $alternates] + set tp_display [join $alternates |] + } + if {[string first | $tp_display] >=0} { + #need to bracket alternate-types to distinguish pipes delimiting flag aliases + set tp_display "($tp_display)" + } + set argdisplay "" + foreach aliasflag [split $argname |] { + if {[string match --* $aliasflag]} { + if {[string index $aliasflag end] eq "="} { + set alias [string range $aliasflag 0 end-1] + if {$value_is_optional} { + append argdisplay "$alias$IS?$NIS=$tp_display$IS?$NIS|" + } else { + append argdisplay "$alias=$tp_display|" + } + } else { + if {$value_is_optional} { + append argdisplay "$aliasflag $IS?$NIS$tp_display$IS?$NIS|" + } else { + append argdisplay "$aliasflag $tp_display|" + } + } } else { - set display "?$argname <$tp>?" + if {$value_is_optional} { + #single dash flag can't accept optional value + append argdisplay "$aliasflag|" + } else { + append argdisplay "$aliasflag $tp_display|" + } } } + set argdisplay [string trimright $argdisplay |] + } + + if {[dict get $arginfo -optional]} { + if {[dict get $arginfo -multiple]} { + set display "?$argdisplay?..." + } else { + set display "?$argdisplay?" + } } else { if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } + set display "$argdisplay ?$argdisplay?..." } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } + set display $argdisplay } } + + #if {[string index $argname end] eq "="} { + # set __ "" + #} else { + # set __ " " + #} + #if {[dict get $arginfo -optional]} { + # if {[dict get $arginfo -multiple]} { + # if {$tp eq "none"} { + # set display "?$argname?..." + # } else { + # set display "?$argname$__$tp_display?..." + # } + # } else { + # if {$tp eq "none"} { + # set display "?$argname?" + # } else { + # set display "?$argname$__$tp_display?" + # } + # } + #} else { + # if {[dict get $arginfo -multiple]} { + # if {$tp eq "none"} { + # set display "$argname ?$argname...?" + # } else { + # set display "$argname$__$tp_display ?$argname$__$tp_display?..." + # } + # } else { + # if {$tp eq "none"} { + # set display $argname + # } else { + # set display "$argname$__$tp_display" + # } + # } + #} append syn " $display" dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] set typelist [dict get $arginfo -type] if {[llength $typelist] == 1} { set tp [lindex $typelist 0] - if {$tp eq "literal"} { - set clause [lindex $argname end] - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set clause $match + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + #set arg_display [dict get $arginfo -typesynopsis] + set clause $ts } else { - set clause $I$argname$RST + #set arg_display $argname + set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { + lappend alternates [lindex $argname end] + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] + lappend alternates $match + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] + lappend alternates $match + } else { + lappend alternates $I$argname$NI + } + } + #remove dupes - but keep order (e.g of dupes -type string|int when no -typesynopsis was specified) + #todo - trie prefixes display + set alternates [punk::args::lib::lunique $alternates] + set clause [join $alternates |] } } else { set n [expr {[llength $typelist]-1}] set name_tail [lrange $argname end-$n end];#if there are enough tail words in the argname to match -types set clause "" - foreach typespec $typelist elementname $name_tail { + set ts [Dict_getdef $arginfo -typesynopsis ""] + if {$ts ne ""} { + set tp_displaylist $ts + } else { + set tp_displaylist [lrepeat [llength $typelist] ""] + } + + foreach typespec $typelist td $tp_displaylist elementname $name_tail { #elementname will commonly be empty if {[string match {\?*\?} $typespec]} { set tp [string range $typespec 1 end-1] @@ -6525,14 +9208,27 @@ tcl::namespace::eval punk::args { set tp $typespec set member_optional 0 } - if {$tp eq "literal"} { - set c $elementname - } elseif {[string match literal(*) $tp]} { - set match [string range $tp 8 end-1] - set c $match - } else { - set c $I$tp$RST + #handle alternate-types e.g literal(text)|literal(binary) + set alternates [list] + foreach tp_alternative [split $tp |] { + if {$tp_alternative eq "literal"} { + lappend alternates $elementname + } elseif {[string match literal(*) $tp_alternative]} { + set match [string range $tp_alternative 8 end-1] + lappend alternates $match + } elseif {[string match literalprefix(*) $tp_alternative]} { + set match [string range $tp_alternative 14 end-1] + lappend alternates $match + } else { + if {$td eq ""} { + lappend alternates $I$tp$NI + } else { + lappend alternates $td + } + } } + set alternates [punk::args::lib::lunique $alternates] + set c [join $alternates |] if {$member_optional} { append clause " " "(?$c?)" } else { @@ -6545,7 +9241,7 @@ tcl::namespace::eval punk::args { set ARGD [dict create argname $argname class value] if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { if {[dict get $arginfo -multiple]} { - #set display "?$I$argname$RST?..." + #set display "?$I$argname$NI?..." set display "?$clause?..." } else { set display "?$clause?" @@ -6554,12 +9250,12 @@ tcl::namespace::eval punk::args { #} elseif {[dict get $arginfo -type] eq "literal"} { # set display "?$argname?" #} else { - # set display "?$I$argname$RST?" + # set display "?$I$argname$NI?" #} } } else { if {[dict get $arginfo -multiple]} { - #set display "$I$argname$RST ?$I$argname$RST?..." + #set display "$I$argname$NI ?$I$argname$NI?..." set display "$clause ?$clause?..." } else { set display $clause @@ -6568,7 +9264,7 @@ tcl::namespace::eval punk::args { #} elseif {[dict get $arginfo -type] eq "literal"} { # set display $argname #} else { - # set display "$I$argname$RST" + # set display "$I$argname$NI" #} } } @@ -6576,9 +9272,11 @@ tcl::namespace::eval punk::args { dict set ARGD type [dict get $arginfo -type] dict set ARGD optional [dict get $arginfo -optional] dict set ARGD display $display - dict lappend SYND $f $ARGD + #dict lappend SYND $f $ARGD + lappend FORMARGS $ARGD } append syn \n + dict set SYND FORMS $f $FORMARGS } switch -- $opt_return { full { @@ -6586,7 +9284,8 @@ tcl::namespace::eval punk::args { } summary { set summary "" - dict for {form arglist} $SYND { + set FORMS [dict get $SYND FORMS] + dict for {form arglist} $FORMS { append summary $id set class_state leader set option_count 0 @@ -6726,6 +9425,26 @@ tcl::namespace::eval punk::args::lib { #[para] Secondary functions that are part of the API #[list_begin definitions] + #tcl86 compat for string is dict - but without -strict or -failindex options + if {[catch {string is dict {}} errM]} { + proc string_is_dict {args} { + #ignore opts + set str [lindex $args end] + if {[catch {[llength $str] len}]} { + return 0 + } + if {$len % 2 == 0} { + return 1 + } + return 0 + } + } else { + proc string_is_dict {args} { + string is dict {*}$args + } + } + + #proc utility1 {p1 args} { # #*** !doctools # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] @@ -6794,7 +9513,10 @@ tcl::namespace::eval punk::args::lib { #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} lappend PUNKARGS [list { @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ + @cmd -name punk::args::lib::tstr\ + -summary\ + "Templating with \$\{$varName\}"\ + -help\ "A rough equivalent of js template literals Substitutions: @@ -7317,6 +10039,18 @@ tcl::namespace::eval punk::args::lib { return $prefix } + #order-preserving + #(same as punk::lib) + proc lunique {list} { + set new {} + foreach item $list { + if {$item ni $new} { + lappend new $item + } + } + return $new + } + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] @@ -7514,7 +10248,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.1.8 + set version 0.2 }] return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm index 3a5f25b0..8d5a5dca 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/config-0.1.tm @@ -449,7 +449,7 @@ tcl::namespace::eval punk::config { Accepts globs eg XDG*" @leaders -min 1 -max 1 #todo - load more whichconfig choices? - whichconfig -type string -choices {config startup-configuration running-configuration} + whichconfig -type any -choices {config startup-configuration running-configuration} @values -min 0 -max -1 globkey -type string -default * -optional 1 -multiple 1 }] @@ -495,7 +495,7 @@ tcl::namespace::eval punk::config { @cmd -name punk::config::configure -help\ "Get/set configuration values from a config" @leaders -min 1 -max 1 - whichconfig -type string -choices {defaults startup-configuration running-configuration} + whichconfig -type any -choices {defaults startup-configuration running-configuration} @values -min 0 -max 2 key -type string -optional 1 newvalue -optional 1 diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm index 19d9d7e4..4322ceaa 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/console-0.1.1.tm @@ -612,10 +612,12 @@ namespace eval punk::console { -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" - -expected_ms -default 100 -type integer -help\ + -expected_ms -default 300 -type integer -help\ "Expected number of ms for response from terminal. 100ms is usually plenty for a local terminal and a - basic query such as cursor position." + basic query such as cursor position. + However on a busy machine a higher timeout may be + prudent." @values -min 2 -max 2 query -type string -help\ "ANSI sequence such as \x1b\[?6n which @@ -680,19 +682,21 @@ namespace eval punk::console { upvar ::punk::console::ansi_response_timeoutid timeoutid set accumulator($callid) "" set waitvar($callid) "" - + lappend queue $callid if {[llength $queue] > 1} { #while {[lindex $queue 0] ne $callid} {} set queuedata($callid) $args set runningid [lindex $queue 0] - while {$runningid ne $callid} { + while {$runningid ne $callid} { + #puts stderr "." vwait ::punk::console::ansi_response_wait set runningid [lindex $queue 0] if {$runningid ne $callid} { set ::punk::console::ansi_response_wait($runningid) $::punk::console::ansi_response_wait($runningid) update ;#REVIEW - probably a bad idea after 10 + set runningid [lindex $queue 0] ;#jn test } } } @@ -779,7 +783,7 @@ namespace eval punk::console { puts "blank extension $waitvar($callid)" puts "->[set $waitvar($callid)]<-" } - puts stderr "get_ansi_response_payload Extending timeout by $extension" + puts stderr "get_ansi_response_payload Extending timeout by $extension for callid:$callid" after cancel $timeoutid($callid) set total_elapsed [expr {[clock millis] - $tslaunch($callid)}] set last_elapsed [expr {[clock millis] - $lastvwait}] @@ -916,7 +920,8 @@ namespace eval punk::console { unset -nocomplain tslaunch($callid) dict unset queuedata $callid - lpop queue 0 + #lpop queue 0 + ledit queue 0 0 if {[llength $queue] > 0} { set next_callid [lindex $queue 0] set waitvar($callid) go_ahead @@ -977,7 +982,7 @@ namespace eval punk::console { set tsnow [clock millis] set total_elapsed [expr {[set tslaunch($callid)] - $tsnow}] set last_elapsed [expr {[set tsclock($callid)] - $tsnow}] - if {[string length $chunks($callid)] % 10 == 0 || $last_elapsed > 16} { + if {[string length $sofar] % 10 == 0 || $last_elapsed > 16} { if {$total_elapsed > 3000} { #REVIEW #too long since initial read handler launched.. @@ -1239,7 +1244,7 @@ namespace eval punk::console { lappend PUNKARGS [list { @id -id ::punk::console::show_input_response @cmd -name punk::console::show_input_response -help\ - "" + "Debug command for console queries using ANSI" @opts -terminal -default {stdin stdout} -type list -help\ "terminal (currently list of in/out channels) (todo - object?)" @@ -1247,9 +1252,9 @@ namespace eval punk::console { "Number of ms to wait for response" @values -min 1 -max 1 request -type string -help\ - "ANSI sequence such as \x1b\[?6n which + {ANSI sequence such as \x1b\[?6n which should elicit a response by the terminal - on stdin" + on stdin} }] proc show_input_response {args} { set argd [punk::args::parse $args withid ::punk::console::show_input_response] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm index 7d1375d7..a95a6242 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/du-0.1.0.tm @@ -70,6 +70,7 @@ namespace eval punk::du { proc du { args } { variable has_twapi + #todo - use punk::args if 0 { switch -exact [llength $args] { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm index 5532ed33..6ce76618 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/lib-0.1.2.tm @@ -301,6 +301,7 @@ tcl::namespace::eval punk::lib::compat { if {"::lpop" ne [info commands ::lpop]} { #puts stderr "Warning - no built-in lpop" interp alias {} lpop {} ::punk::lib::compat::lpop + punk::args::set_alias ::punk::lib::compat::lpop ::lpop ;#point to the definition of ::lpop defined in punk::args::tclcore } proc lpop {lvar args} { #*** !doctools @@ -339,6 +340,51 @@ tcl::namespace::eval punk::lib::compat { set l $newlist return $v } + if {"::ledit" ni [info commands ::ledit]} { + interp alias {} ledit {} ::punk::lib::compat::ledit + punk::args::set_alias ::punk::lib::compat::ledit ::ledit + } + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [punk::lib::lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [punk::lib::lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } #slight isolation - varnames don't leak - but calling context vars can be affected @@ -695,14 +741,15 @@ namespace eval punk::lib { proc lswap {lvar a z} { upvar $lvar l - if {[lindex_resolve_basic $l $a] < 0 || [lindex_resolve_basic $l $z] < 0} { + set len [llength $l] + if {[lindex_resolve_basic $len $a] < 0 || [lindex_resolve_basic $len $z] < 0} { #lindex_resolve_basic returns only -1 if out of range #if we didn't do this check - we could raise an error on second lset below - leaving list corrupted because only one lset occurred #(e.g using: lswap mylist end-2 end on a two element list) #on the unhapy path we can take time to check the nature of the out-of-boundness to give a nicer report #use full 'lindex_resolve' which can report which side via -3 and -2 special results being lower and upper bound breaches respectively (-1 never returned) - set a_index [lindex_resolve $l $a] + set a_index [lindex_resolve $len $a] set a_msg "" switch -- $a_index { -2 { @@ -712,7 +759,7 @@ namespace eval punk::lib { set a_msg "1st supplied index $a is below the lower bound for the list (0)" } } - set z_index [lindex_resolve $l $z] + set z_index [lindex_resolve $len $z] set z_msg "" switch -- $z_index { -2 { @@ -1100,7 +1147,7 @@ namespace eval punk::lib { - then the normal = separator will be replaced with a coloured (or underlined if colour off) 'mismatch' indicator. e.g4 set list {{k1 v1 k2 v2} {k1 vv1 k2 vv2}}; pdict list @0-end/@@k2 @*/@@k1 Here we supply 2 separate pattern hierarchies, where @0-end and @* are list operations and are equivalent - The second level segement in each pattern switches to a dict operation to retrieve the value by key. + The second level segment in each pattern switches to a dict operation to retrieve the value by key. When a list operation such as @* is used - integer list indexes are displayed on the left side of the = for that hierarchy level. } }] @@ -1137,11 +1184,13 @@ namespace eval punk::lib { if {!$has_punk_ansi} { set RST "" set sep " = " - set sep_mismatch " mismatch " + #set sep_mismatch " mismatch " + set sep \u2260 ;# equivalent [punk::ansi::convert_g0 [punk::ansi::g0 |]] (not equal symbol) } else { set RST [punk::ansi::a] set sep " [punk::ansi::a+ Green]=$RST " ;#stick to basic default colours for wider terminal support - set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + #set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]mismatch$RST " + set sep_mismatch " [punk::ansi::a+ Brightred undercurly underline undt-white]\u2260$RST " } package require punk::pipe #package require punk ;#we need pipeline pattern matching features @@ -1173,6 +1222,7 @@ namespace eval punk::lib { -keysortdirection -default increasing -choices {increasing decreasing} -debug -default 0 -type boolean -help\ "When enabled, produces some rudimentary debug output on stderr" + -- -type none -optional 1 @values -min 1 -max -1 dictvalue -type list -help\ "dict or list value" @@ -1465,7 +1515,7 @@ namespace eval punk::lib { if {![regexp $re_idxdashidx $p _match a b]} { error "unrecognised pattern $p" } - set lower_resolve [punk::lib::lindex_resolve $dval $a] ;#-2 for too low, -1 for too high + set lower_resolve [punk::lib::lindex_resolve [llength $dval] $a] ;#-2 for too low, -1 for too high #keep lower_resolve as separate var to lower for further checks based on which side out-of-bounds if {${lower_resolve} == -2} { ##x @@ -1478,7 +1528,7 @@ namespace eval punk::lib { } else { set lower $lower_resolve } - set upper [punk::lib::lindex_resolve $dval $b] + set upper [punk::lib::lindex_resolve [llength $dval] $b] if {$upper == -3} { ##x #upper bound is below list range - @@ -1831,7 +1881,8 @@ namespace eval punk::lib { if {$last_hidekey} { append result \n } - append result [textblock::join_basic -- $kblock $sblock $vblock] \n + #append result [textblock::join_basic -- $kblock $sblock $vblock] \n + append result [textblock::join_basic_raw $kblock $sblock $vblock] \n } set last_hidekey $hidekey incr kidx @@ -1880,6 +1931,19 @@ namespace eval punk::lib { } proc is_list_all_in_list {small large} { + if {[llength $small] > [llength $large]} {return 0} + foreach x $large { + ::set ($x) {} + } + foreach x $small { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } + #v2 generally seems slower + proc is_list_all_in_list2 {small large} { set small_in_large [lsort [struct::set intersect [lsort -unique $small] $large ]] return [struct::list equal [lsort $small] $small_in_large] } @@ -1888,11 +1952,22 @@ namespace eval punk::lib { package require struct::list package require struct::set } - append body [info body is_list_all_in_list] - proc is_list_all_in_list {small large} $body + append body [info body is_list_all_in_list2] + proc is_list_all_in_list2 {small large} $body } - proc is_list_all_ni_list {a b} { + proc is_list_all_ni_list {A B} { + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {[info exists ($x)]} { + return 0 + } + } + return 1 + } + proc is_list_all_ni_list2 {a b} { set i [struct::set intersect $a $b] return [expr {[llength $i] == 0}] } @@ -1900,8 +1975,8 @@ namespace eval punk::lib { set body { package require struct::list } - append body [info body is_list_all_ni_list] - proc is_list_all_ni_list {a b} $body + append body [info body is_list_all_ni_list2] + proc is_list_all_ni_list2 {a b} $body } #somewhat like struct::set difference - but order preserving, and doesn't treat as a 'set' so preserves dupes in fromlist @@ -1917,7 +1992,16 @@ namespace eval punk::lib { } return $result } + #with ledit (also avail in 8.6 using punk::lib::compat::ledit proc ldiff2 {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + foreach item $removeitems { + set posns [lsearch -all -exact $fromlist $item] + foreach p $posns {ledit fromlist $p $p} + } + return $fromlist + } + proc ldiff3 {fromlist removeitems} { set doomed [list] foreach item $removeitems { lappend doomed {*}[lsearch -all -exact $fromlist $item] @@ -2158,35 +2242,75 @@ namespace eval punk::lib { } } - # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bunds on upper vs lower side - proc lindex_resolve {list index} { + # showdict uses lindex_resolve results -2 & -3 to determine whether index is out of bounds on upper vs lower side + #REVIEW: This shouldn't really need the list itself - just the length would suffice + punk::args::define { + @id -id ::punk::lib::lindex_resolve + @cmd -name punk::lib::lindex_resolve\ + -summary\ + "Resolve an indexexpression to an integer based on supplied list or string length."\ + -help\ + "Resolve an index which may be of the forms accepted by Tcl list or string commands such as end-2 or 2+2 + to the actual integer index for the supplied list/string length, or to a negative value below -1 indicating + whether the index was below or above the range of possible indices for the length supplied. + + Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. + This means the proc may be called with something like $x+2 end-$y etc + Sometimes the actual integer index is desired. + + We want to resolve the index used, without passing arbitrary expressions into the 'expr' function + - which could have security risks. + lindex_resolve will parse the index expression and return: + a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) + b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) + lindex_resolve never returns -1 - as the similar function lindex_resolve_basic uses this to denote + out of range at either end of the list/string. + Otherwise it will return an integer corresponding to the position in the data. + This is in stark contrast to Tcl list/string function indices which will return empty strings for out of + bounds indices, or in the case of lrange, return results anyway. + Like Tcl list commands - it will produce an error if the form of the index is not acceptable. + For empty lists/string (datalength 0), end and end+x indices are considered to be out of bounds on the upper side + - thus returning -2 + + Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. + We will get something like 10+1 - which can be resolved safely with expr + " + @values -min 2 -max 2 + datalength -type integer + index -type indexexpression + } + proc lindex_resolve {len index} { #*** !doctools - #[call [fun lindex_resolve] [arg list] [arg index]] - #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list - #[para]Users may define procs which accept a list index and wish to accept the forms understood by Tcl. + #[call [fun lindex_resolve] [arg len] [arg index]] + #[para]Resolve an index which may be of the forms accepted by Tcl list commands such as end-2 or 2+2 to the actual integer index for the supplied list/string length + #[para]Users may define procs which accept a list/string index and wish to accept the forms understood by Tcl. #[para]This means the proc may be called with something like $x+2 end-$y etc #[para]Sometimes the actual integer index is desired. #[para]We want to resolve the index used, without passing arbitrary expressions into the 'expr' function - which could have security risks. #[para]lindex_resolve will parse the index expression and return: #[para] a) -3 if the supplied index expression is below the lower bound for the supplied list. (< 0) #[para] b) -2 if the supplied index expression is above the upper bound for the supplied list. (> end) - #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list + #[para] We don't return -1 - as the similar function lindex_resolve_basic uses this to denote out of range at either end of the list/string #[para]Otherwise it will return an integer corresponding to the position in the list. - #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out or bounds indices, or in the case of lrange, return results anyway. + #[para]This is in stark contrast to Tcl list function indices which will return empty strings for out of bounds indices, or in the case of lrange, return results anyway. #[para]Like Tcl list commands - it will produce an error if the form of the index is not acceptable #[para]For empty lists, end and end+x indices are considered to be out of bounds on the upper side - thus returning -2 - #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which we will resolve (hopefully safely) with expr + #Note that for an index such as $x+1 - we never see the '$x' as it is substituted in the calling command. We will get something like 10+1 - which can be resolved safely with expr #if {![llength $list]} { # #review # return ??? #} + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } else { #integer may still have + sign - normalize with expr @@ -2203,7 +2327,7 @@ namespace eval punk::lib { } } else { #index is 'end' - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { #special case - 'end' with empty list - treat end like a positive number out of bounds return -2 @@ -2212,7 +2336,7 @@ namespace eval punk::lib { } } if {$offset == 0} { - set index [expr {[llength $list]-1}] + set index [expr {$len-1}] if {$index < 0} { return -2 ;#special case as above } else { @@ -2220,7 +2344,7 @@ namespace eval punk::lib { } } else { #by now, if op = + then offset = 0 so we only need to handle the minus case - set index [expr {([llength $list]-1) - $offset}] + set index [expr {($len-1) - $offset}] } if {$index < 0} { return -3 @@ -2245,33 +2369,32 @@ namespace eval punk::lib { } if {$index < 0} { return -3 - } elseif {$index >= [llength $list]} { + } elseif {$index >= $len} { return -2 } return $index } } } - proc lindex_resolve_basic {list index} { + proc lindex_resolve_basic {len index} { #*** !doctools - #[call [fun lindex_resolve_basic] [arg list] [arg index]] + #[call [fun lindex_resolve_basic] [arg len] [arg index]] #[para] Accepts index of the forms accepted by Tcl's list commands. (e.g compound indices such as 3+1 end-2) #[para] returns -1 for out of range at either end, or a valid integer index #[para] Unlike lindex_resolve; lindex_resolve_basic can't determine if an out of range index was out of range at the lower or upper bound - #[para] This is only likely to be faster than average over lindex_resolve for Tcl which has the builtin lseq command + #[para] This is only likely to be faster than average over lindex_resolve for small lists and for Tcl which has the builtin lseq command #[para] The performance advantage is more likely to be present when using compound indexes such as $x+1 or end-1 #[para] For pure integer indices the performance should be equivalent - #set indices [list] ;#building this may be somewhat expensive in terms of storage and compute for large lists - we could use lseq in Tcl 8.7+ - # - which - #for {set i 0} {$i < [llength $list]} {incr i} { - # lappend indices $i - #} + if {![string is integer -strict $len]} { + error "lindex_resolve_basic len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 if {[string is integer -strict $index]} { #can match +i -i #avoid even the lseq overhead when the index is simple - if {$index < 0 || ($index >= [llength $list])} { + if {$index < 0 || ($index >= $len)} { #even though in this case we could return -2 or -3 like lindex_resolve; for consistency we don't, as it's not always determinable for compound indices using the lseq method. return -1 } else { @@ -2279,13 +2402,15 @@ namespace eval punk::lib { return [expr {$index}] } } - if {[llength $list]} { - set indices [punk::lib::range 0 [expr {[llength $list]-1}]] ;# uses lseq if available, has fallback. - #if lseq was available - $indices is an 'arithseries' - theoretically not taking up ram(?) + if {$len > 0} { + #For large len - this is a wasteful allocation if no true lseq available in Tcl version. + #lseq produces an 'arithseries' object which we can index into without allocating an entire list (REVIEW) + set testlist [punk::lib::range 0 [expr {$len-1}]] ;# uses lseq if available, has fallback. } else { - set indices [list] + set testlist [list] + #we want to call 'lindex' even in this case - to get the appropriate error message } - set idx [lindex $indices $index] + set idx [lindex $testlist $index] if {$idx eq ""} { #we have no way to determine if out of bounds is at lower vs upper end return -1 @@ -2304,6 +2429,81 @@ namespace eval punk::lib { } } + proc string_splitbefore {str index} { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + return [list $str ""] + } + -3 { + return [list "" $str] + } + } + } + return [list [string range $str 0 $index-1] [string range $str $index end]] + #scan %s stops at whitespace - not useful here. + #scan $s %${p}s%s + } + proc string_splitbefore_indices {str args} { + set parts [list $str] + set sizes [list [string length $str]] + set s 0 + foreach index $args { + if {![string is integer -strict $index]} { + set index [punk::lib::lindex_resolve [string length $str] $index] + switch -- $index { + -2 { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + -3 { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + } + } + if {$index <= 0} { + if {[lindex $sizes 0] != 0} { + ledit parts 0 0 {} [lindex $parts 0] + ledit sizes 0 0 0 [lindex $sizes 0] + } + continue + } + if {$index >= [string length $str]} { + if {[lindex $sizes end] != 0} { + ledit parts end end [lindex $parts end] {} + ledit sizes end end [lindex $sizes end] 0 + } + continue + } + set i -1 + set a 0 + foreach sz $sizes { + incr i + if {$a + $sz > $index} { + set p [lindex $parts $i] + #puts "a:$a index:$index" + if {$a == $index} { + break + } + ledit parts $i $i [string range $p 0 [expr {$index -$a -1}]] [string range $p $index-$a end] + ledit sizes $i $i [expr {$index - $a}] [expr {($a + $sz)-$index}] + break + } + incr a $sz + } + #puts "->parts:$parts" + #puts "->sizes:$sizes" + } + return $parts + } proc K {x y} {return $x} #*** !doctools @@ -3133,8 +3333,7 @@ namespace eval punk::lib { #Each resulting line should have a reset of some type at start and a pure-reset at end to stop #see if we can find an ST sequence that most terminals will not display for marking sections? if {$opt_ansireplays} { - #package require punk::ansi - + ;#package require punk::ansi if {$opt_ansiresets} { set RST "\x1b\[0m" } else { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm index 6f01e340..19d5177d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/libunknown-0.1.tm @@ -81,14 +81,15 @@ tcl::namespace::eval punk::libunknown { }] variable epoch - if {![info exists epoch]} { - set tmstate [dict create 0 {}] - set pkgstate [dict create 0 {}] - set tminfo [dict create current 0 epochs $tmstate] - set pkginfo [dict create current 0 epochs $pkgstate] + #if {![info exists epoch]} { + # set tmstate [dict create 0 {}] + # set pkgstate [dict create 0 {}] + # set tminfo [dict create current 0 epochs $tmstate] + # set pkginfo [dict create current 0 epochs $pkgstate] + + # set epoch [dict create tm $tminfo pkg $pkginfo] + #} - set epoch [dict create tm $tminfo pkg $pkginfo] - } variable has_package_files if {[catch {package files foobaz}]} { @@ -114,7 +115,19 @@ tcl::namespace::eval punk::libunknown { # Import the pattern used to check package names in detail. variable epoch set pkg_epoch [dict get $epoch tm current] - + set must_scan 0 + if {[dict exists $epoch tm untracked $name]} { + set must_scan 1 + #a package that was in the package database at the start - is now being searched for as unknown + #our epoch info is not reliable for pre-known packages - so increment the epoch and fully clear the 'added' paths even in zipfs to do proper scan + + #review + #epoch_incr_pkg clearadded + #epoch_incr_tm clearadded + #puts ">>>> removing untracked tm: $name" + dict unset epoch tm untracked $name + #whie it is not the most common configuration - a package could be provided both as a .tm and by packageIndex.tcl files + } #variable paths upvar ::tcl::tm::paths paths @@ -151,7 +164,8 @@ tcl::namespace::eval punk::libunknown { if {![interp issafe] && ![file exists $path]} { continue } - set currentsearchpath [file join $path $pkgroot] + set currentsearchpath $path + set specificsearchpath [file join $path $pkgroot] # Get the module files out of the subdirectories. # - Safe Base interpreters have a restricted "glob" command that @@ -162,32 +176,35 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $path]} { - if {[dict exists $epoch tm epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch tm epochs $pkg_epoch indexes $specificsearchpath]} { #indexes are actual .tm files here - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles ( cached ): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" } else { - if {![interp issafe] && ![file exists $currentsearchpath]} { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + if {![interp issafe] && ![file exists $specificsearchpath]} { + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] continue } - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath [dict create] + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath [dict create] # ################################################################# if {$has_zipfs && [string match $zipfsroot* $path]} { + #The entire tm tre is available so quickly from the zipfs::list call - that we can gather all at once. set tmfiles [::tcl::zipfs::list $currentsearchpath/*.tm] ;#could theoretically be a dir - this is effectively a tree traversal foreach tm_path $tmfiles { dict set epoch tm epochs $pkg_epoch indexes [file dirname $tm_path] $tm_path $pkg_epoch } - #retrieval using tcl::zipfs::list got (and cached) extras - limit to currentsearchpath - set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $currentsearchpath]] + #retrieval using tcl::zipfs::list got (and cached) extras - limit to specificsearchpath + set tmfiles [dict keys [dict get $epoch tm epochs $pkg_epoch indexes $specificsearchpath]] } else { - set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + #set tmfiles [glob -nocomplain -directory $currentsearchpath *.tm] + set tmfiles [glob -nocomplain -directory $specificsearchpath *.tm] foreach tm_path $tmfiles { - dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + #dict set epoch tm epochs $pkg_epoch indexes $currentsearchpath $tm_path $pkg_epoch + dict set epoch tm epochs $pkg_epoch indexes $specificsearchpath $tm_path $pkg_epoch } } #puts "--->zipfs_tm_UnknownHandler $tid llength tmfiles (UNcached): [format %4d [llength $tmfiles]] name:$name searchpath:$currentsearchpath" @@ -203,8 +220,8 @@ tcl::namespace::eval punk::libunknown { set can_skip_update 0 if {[string match $zipfsroot* $path]} { #static tm location - if {[dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch tm epochs $pkg_epoch added $currentsearchpath $name]} { + if {[dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath]} { + if {![dict exists $epoch tm epochs $pkg_epoch added $specificsearchpath $name]} { #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP orig:$original name:$name args:$args searchpath:$currentsearchpath" set can_skip_update 1 @@ -213,19 +230,13 @@ tcl::namespace::eval punk::libunknown { #dict unset epoch tm epochs $pkg_epoch added $currentsearchpath $name } } - } else { - #dynamic - can only skip if negatively cached for the current epoch - if {[dict exists $epoch tm epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tm_UnknownHandler $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_update 1 - } - } + if {!$can_skip_update} { set strip [llength [file split $path]] set found_name_in_currentsearchpath 0 ;#for negative cache by epoch - catch { + if {[catch { foreach file $tmfiles { set pkgfilename [join [lrange [file split $file] $strip end] ::] @@ -252,6 +263,20 @@ tcl::namespace::eval punk::libunknown { # the one we already have. # This does not apply to Safe Base interpreters because # the token-to-directory mapping may have changed. + + #JMN - review. + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } + if {$pkgname eq $name} { + #can occur multiple times, different versions + #record package name as found in this path whether version satisfies or not + set found_name_in_currentsearchpath 1 + } + #don't override the ifneeded script - for tm files the first encountered 'wins'. continue } @@ -273,8 +298,15 @@ tcl::namespace::eval punk::libunknown { "[::list package provide $pkgname $pkgversion];[::list source $file]" #JMN - #store only once for each name, although there may be multiple versions - dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkg_epoch + #store only once for each name, although there may be multiple versions of same package within this searchpath + #dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname [dict create e $pkg_epoch v $pkgversion] + dict set epoch tm epochs $pkg_epoch added $currentsearchpath $pkgname $pkgversion e$pkg_epoch + #pkgname here could be the 'name' passed at the beggning - or other .tms at the same location. + #we can't always remove other .tms from 'tm untracked' because the search for name might skip some locations. + if {$must_scan} { + #however - if we know we're forced to scan all tm paths we can remove discovered sibling tms from tm untracked + dict unset epoch tm untracked $pkgname + } # We abort in this unknown handler only if we got a # satisfying candidate for the requested package. @@ -298,10 +330,8 @@ tcl::namespace::eval punk::libunknown { set found_name_in_currentsearchpath 1 } } - } - if {!$found_name_in_currentsearchpath} { - #can record as unfound for this path - for this epoch - dict set epoch tm epochs $pkg_epoch unfound $currentsearchpath $name 1 + } errMsg]} { + puts stderr "zipfs_tm_Unknownhandler: error for tm file $file searchpath:$currentsearchpath" } } @@ -380,9 +410,9 @@ tcl::namespace::eval punk::libunknown { } - if {$satisfied} { - ##return - } + #if {$satisfied} { + # return + #} } # Fallback to previous command, if existing. See comment above about @@ -399,6 +429,25 @@ tcl::namespace::eval punk::libunknown { variable epoch set pkg_epoch [dict get $epoch pkg current] + #review - the ifneeded script is not the only thing required in a new interp.. consider tclIndex files and auto_load mechanism. + #also the pkgIndex.tcl could possibly provide a different ifneeded script based on interp issafe (or other interp specific things?) + #if {[dict exists $epoch scripts $name]} { + # set vscripts [dict get $epoch scripts $name] + # dict for {v scr} $vscripts { + # puts ">package ifneeded $name $v" + # package ifneeded $name $v $scr + # } + # return + #} + set must_scan 0 + if {[dict exists $epoch pkg untracked $name]} { + #a package that was in the package database at the start - is now being searched for as unknown + #(due to a package forget?) + #our epoch info is not valid for pre-known packages - so setting must_scan to true + set must_scan 1 + #puts ">>>> removing pkg untracked: $name" + dict unset epoch pkg untracked $name + } #global auto_path env global auto_path @@ -414,7 +463,7 @@ tcl::namespace::eval punk::libunknown { set zipfsroot [tcl::zipfs::root] set has_zipfs 1 } else { - set zipfsroot "//zipfs:/" ;#doesn't matter much what we use here - don't expect in tm list if no zipfs commands + set zipfsroot "//zipfs:/" ;#doesn't matter too much what we use here - don't expect in tm list if no zipfs commands set has_zipfs 0 } @@ -425,6 +474,14 @@ tcl::namespace::eval punk::libunknown { #question is whether some pkgIndex.tcl files may do a package forget? They probably don't/shouldn't(?) Does that matter here anyway? set before_dict [dict create] + #J2 + #siblings that have been affected by source scripts - need to retest ifneeded scripts at end for proper ordering. + set refresh_dict [dict create] + + #Note that autopath is being processed from the end to the front + #ie last lappended first. This means if there are duplicate versions earlier in the list, + #they will be the last to call 'package provide' for that version and so their provide script will 'win'. + #This means we should have faster filesystems such as zipfs earlier in the list. # Cache the auto_path, because it may change while we run through the # first set of pkgIndex.tcl files @@ -432,6 +489,7 @@ tcl::namespace::eval punk::libunknown { while {[llength $use_path]} { set dir [lindex $use_path end] + # Make sure we only scan each directory one time. if {[info exists tclSeenPath($dir)]} { set use_path [lrange $use_path 0 end-1] @@ -449,7 +507,7 @@ tcl::namespace::eval punk::libunknown { set use_epoch_for_all 1 if {$use_epoch_for_all || [string match $zipfsroot* $dir]} { set currentsearchpath $dir - if {[dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { + if {!$must_scan && [dict exists $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]} { set indexfiles [dict keys [dict get $epoch pkg epochs $pkg_epoch indexes $currentsearchpath]] #puts stderr "--->zipfs_tclPkgUnknown $tid llength tmfiles ( cached ): [format %4d [llength $indexfiles]] name:$name searchpath:$currentsearchpath" } else { @@ -468,29 +526,26 @@ tcl::namespace::eval punk::libunknown { } set can_skip_sourcing 0 - if {$has_zipfs && [string match $zipfsroot* $dir]} { + #if {$has_zipfs && [string match $zipfsroot* $dir]} { #static auto_path dirs - #can avoid scan if added via this path in any epoch - if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { - if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" - set can_skip_sourcing 1 - } else { - #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? - #remove it and let it be readded if it's still provided by this path? - #probably doesn't make sense for static path? - #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + if {!$must_scan} { + if {[dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath]} { + if {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { + #$name wasn't provided by this path before - in static zipfs it shouldn't be necessary to look here again. + #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath" + set can_skip_sourcing 1 + } else { + #if this name is in added then we must have done a package forget or it wouldn't come back to package unknown ? + #remove it and let it be readded if it's still provided by this path? + #probably doesn't make sense for static path? + #dict unset epoch pkg epochs $pkg_epoch added $currentsearchpath $name + } } } - } else { - #dynamic auto_path dirs - libs could have been added/removed - #scan unless cached negative for this epoch - if {[dict exists $epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name]} { - #puts stderr "zipfs_tclPkgUnknown $tid CAN SKIP $name currentsearchpath:$currentsearchpath (unfound already in epoch $pkg_epoch)" - set can_skip_sourcing 1 - } - } + #} + + + #An edge case exception is that after a package forget, a deliberate call to 'package require non-existant' #will not trigger rescans for all versions of other packages. #A rescan of a specific package for all versions can still be triggered with a package require for @@ -498,33 +553,53 @@ tcl::namespace::eval punk::libunknown { #(or misordered min max e.g package require md5 1-0 i.e a deliberately unsatisfiable version range) set sourced 0 + set just_added [dict create] + set just_changed [dict create] + #set sourced_files [list] + + #J2 + #set can_skip_sourcing 0 + if {!$can_skip_sourcing} { #Note - naive comparison of before_pkgs vs after_pkgs isn't quite enough to tell if something was added. It could have added a version. #this will stop us rescanning everything properly by doing a 'package require nonexistant' - #use 'info exists' to only call package names once and then append? worth it? + #use 'info exists' to only call package names once and then append? + #This could be problematic? (re-entrant tclPkgUnknown in some pkgIndex scripts?) pkgIndex.tcl scripts "shouldn't" do this? if {![info exists before_pkgs]} { set before_pkgs [package names] + #update the before_dict which persists across while loop + #we need to track the actual 'ifneeded' script not just version numbers, + #because the last ifneeded script processed for each version is the one that ultimately applies. + foreach bp $before_pkgs { + #dict set before_dict $bp [package versions $bp] + foreach v [package versions $bp] { + dict set before_dict $bp $v [package ifneeded $bp $v] + } + } } - #update the before_dict which persists across while loop - foreach bp $before_pkgs { - dict set before_dict $bp [package versions $bp] - } - catch { + #set before_pkgs [package names] + + #catch { foreach file $indexfiles { set dir [file dirname $file] if {![info exists procdDirs($dir)]} { try { - #puts stderr "----->0 sourcing $file" + #if {[string match //zipfs*registry* $file]} { + # puts stderr "----->0 sourcing zipfs file $file" + #} incr sourced ;#count as sourced even if source fails; keep before actual source action #::tcl::Pkg::source $file + #lappend sourced_files $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (1)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (1)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" @@ -532,8 +607,11 @@ tcl::namespace::eval punk::libunknown { set procdDirs($dir) 1 } } + #each source operation could affect auto_path - and thus increment the pkg epoch (via trace on ::auto_path) + #e.g tcllib pkgIndex.tcl appends to auto_path + set pkg_epoch [dict get $epoch pkg current] } - } + #} set dir [lindex $use_path end] if {![info exists procdDirs($dir)]} { set file [file join $dir pkgIndex.tcl] @@ -542,20 +620,24 @@ tcl::namespace::eval punk::libunknown { try { #puts "----->2 sourcing $file" incr sourced + #lappend sourced_files $file #::tcl::Pkg::source $file tcl_Pkg_source $file } trap {POSIX EACCES} {} { # $file was not readable; silently ignore + puts stderr "zipfs_tclPkgUnknown file unreadable '$file' while trying to load $name (2)" continue } on error msg { if {[regexp {version conflict for package} $msg]} { # In case of version conflict, silently ignore + puts stderr "zipfs_tclPkgUnknown version conflict sourcing '$file' while trying to load $name (2)\nmsg:$msg" continue } tclLog "error reading package index file $file: $msg" } on ok {} { set procdDirs($dir) 1 } + set pkg_epoch [dict get $epoch pkg current] } } #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath [dict create] @@ -568,34 +650,89 @@ tcl::namespace::eval punk::libunknown { } set after_pkgs [package names] - set just_added [dict create] + #puts "@@@@pkg epochs $pkg_epoch searchpath:$currentsearchpath name:$name before: [llength $before_pkgs] after: [llength $after_pkgs]" if {[llength $after_pkgs] > [llength $before_pkgs]} { foreach a $after_pkgs { - if {![dict exists $before_dict $a]} { - dict set just_added $a 1 - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $pkg_epoch + foreach v [package versions $a] { + if {![dict exists $before_dict $a $v]} { + dict set just_added $a $v 1 + set iscript [package ifneeded $a $v] + #J2 + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a [dict create e $pkg_epoch v $v] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $a $v [dict create e $pkg_epoch scr $iscript] + if {$must_scan} { + dict unset epoch pkg untracked $a + } + } } } - #puts stderr ">>>zipfs_tclPkgUnknown added [llength $added_pkgs]" - #puts stderr ">>> [join [lrange $added_pkgs 0 10] \n]..." } - dict for {bp bpversions} $before_dict { - if {[dict exists $just_added $bp]} { - continue - } - if {[llength $bpversions] != [llength [package versions $bp]]} { - dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $pkg_epoch + + #----------------- + #if {[dict size $just_added]} { + # puts stderr "\x1b\[31m>>>zipfs_tclPkgUnknown called on name:$name added [dict size $just_added] from searchpath:$currentsearchpath\x1b\[m" + # puts stderr ">>> [join [lrange [dict keys $just_added] 0 10] \n]..." + #} else { + # tclLog ">>>zipfs_tclPkgUnknown called on name:$name Nothing added for searchpath:$currentsearchpath" + # if {[string match twapi* $name]} { + # tclLog ">>>zipfs_tclPkgUnknown: sourced_files:" + # foreach f $sourced_files { + # puts ">>> $f" + # } + # } + # if {$currentsearchpath in "//zipfs:/app //zipfs:/app/tcl_library"} { + # puts " before_pkgs: [llength $before_pkgs]" + # puts " lsearch msgcat: [lsearch $before_pkgs msgcat]" + # puts " after_pkgs: [llength $after_pkgs]" + # puts " \x1b\31mlsearch msgcat: [lsearch $after_pkgs msgcat]\x1b\[m" + # if {[lsearch $after_pkgs msgcat] >=0} { + # set versions [package versions msgcat] + # puts "msgcat versions: $versions" + # foreach v $versions { + # puts "\x1b\[32m $v ifneeded: [package ifneeded msgcat $v] \x1b\[m" + # } + # } + # } + #} + #----------------- + + #review - just because this searchpath didn't add a package or add a version for the package + #it doesn't mean there wasn't a version of this package supplied there + #It may just be the same version as one we've already found. + #The last one found (earlier in auto_path) for a version is the one that supplies the final 'package provide' statement (by overriding it) + # + dict for {bp bpversionscripts} $before_dict { + #if {!$must_scan && ![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $bp]} { + # #puts -nonewline . + # continue + #} + dict for {bv bscript} $bpversionscripts { + set nowscript [package ifneeded $bp $bv] + if {$bscript ne $nowscript} { + #ifneeded script has changed. The same version of bp was supplied on this path. + #As it's processed later - it will be the one in effect. + #dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp [dict create e $pkg_epoch v $bv] + dict set epoch pkg epochs $pkg_epoch added $currentsearchpath $bp $bv [dict create e $pkg_epoch scr $nowscript] + dict set before_dict $bp $bv $nowscript + dict set just_changed $bp $bv 1 + #j2 + if {$must_scan} { + dict unset epoch pkg untracked $bp + } + } } } - #puts stderr "zipfs_tclPkgUnknown $tid sourced: $sourced (under path: $currentsearchpath)" - if {$name ni $after_pkgs} { - #cache negative result (for this epoch only) - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } elseif {![dict exists $epoch pkg epochs $pkg_epoch added $currentsearchpath $name]} { - dict set epoch pkg epochs $pkg_epoch unfound $currentsearchpath $name 1 - } - lappend before_pkgs {*}[dict keys $just_added] + #update before_pkgs & before_dict for next path + dict for {newp vdict} $just_added { + if {$newp ni $before_pkgs} { + lappend before_pkgs $newp + } + dict for {v _} $vdict { + set nowscript [package ifneeded $newp $v] + dict set before_dict $newp $v $nowscript + } + } } } @@ -680,20 +817,143 @@ tcl::namespace::eval punk::libunknown { } } set old_path $auto_path + + dict for {pkg versions} $just_changed { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + dict for {pkg versions} $just_added { + foreach v [dict keys $versions] { + dict set refresh_dict $pkg $v 1 + } + } + } + + #refresh ifneeded scripts for just_added/just_changed + #review: searchpaths are in auto_path order - earliest has precedence for any particular pkg-version + + #REVIEW: what is to stop an auto_path package e.g from os, overriding a .tm ifneeded script from an item earlier in the package_mode list configured in punk's main.tcl? + #e.g when package_mode is {dev-os} we don't want a pkgIndex package from ::env(TCLLIBPATH) overriding a .tm from the dev paths (even if version nums the same) + #conversely we do want a dev path pkIndex package overriding an existing ifneeded script from a .tm in os + #to accomodate this - we may need to maintain a subdict in epoch of paths/path-prefixes to package_mode members os, dev, internal + + #this 'refresh' is really a 'reversion' to what was already stored in epoch pkg epochs added + # + + set e [dict get $epoch pkg current] + set pkgvdone [dict create] + set dict_added [dict get $epoch pkg epochs $e added] + #keys are in reverse order due to tclPkgUnknown processing order + set ordered_searchpaths [lreverse [dict keys $dict_added]];# ordered as in auto_path + + dict for {pkg versiond} $refresh_dict { + set versions [dict keys $versiond] + puts stderr "---->pkg:$pkg versions: $versions" + foreach searchpath $ordered_searchpaths { + set addedinfo [dict get $dict_added $searchpath] + set vidx -1 + foreach v $versions { + incr vidx + if {[dict exists $addedinfo $pkg $v]} { + ledit versions $vidx $vidx + set iscript [dict get $addedinfo $pkg $v scr] + #todo - find the iscript in the '$epoch pkg epochs added paths' lists and determine os vs dev vs internal + #(scanning for path directly in the ifneeded script for pkgs is potentially error prone) + #for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway) + set justaddedscript [package ifneeded $pkg $v] + if {$justaddedscript ne $iscript} { + puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions" + package ifneeded $pkg $v $iscript + #dict set pkgvdone $pkg $v 1 + } + } + } + if {[llength $versions] == 0} { + break + } + } } + + #puts "zipfs_tclPkgUnknown DONE" } + variable last_auto_path + variable last_tm_paths proc epoch_incr_pkg {args} { if {[catch { + variable last_auto_path global auto_path upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] + # ------------- + puts stderr "--> pkg epoch $prev_e -> $current_e" + puts stderr "args: $args" + puts stderr "last_auto: $last_auto_path" + puts stderr "auto_path: $auto_path" + # ------------- + if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { + #The auto_path changed, and is a pure addition of entry/entries + #commonly this is occurs where a single entry is added by a pkgIndex.Tcl + #e.g tcllib adds its base dir so that all pkgIndex.tcl files in subdirs are subsequently found + #consider autopath + #c:/libbase //zipfs:/app/libbase + #if both contain a tcllib folder with pkgIndex.tcl that extends auto_path, the auto_path extends as follows: + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib + # -> c:/libbase //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase/tcllib + + #the tclPkgUnknown usedir loop (working from end of list towards beginning) will process these changes the first time dynamically + #as they occur: + #ie //zipfs:/app/libbase //zipfs:/app/libbase/tcllib c:/libbase c:/libbase/tcllib + + #A subsequent scan by tclPkgUnknown on the extended auto_path would process in the order: + #c:/libbase/tcllib c:/libbase //zipfs:/app/libbase/tcllib //zipfs:/app/libbase + + #re-order the new additions to come immediately following the longest common prefix entry + + set newitems [punk::libunknown::lib::ldiff $auto_path $last_auto_path] + + set update $last_auto_path + #no ledit or punk::lib::compat::ledit for 8.6 - so use linsert + foreach new $newitems { + set offset 0 + set has_prefix 0 + foreach ap [lreverse $update] { + if {[string match $ap* $new]} { + set has_prefix 1 + break + } + incr offset + } + if {$has_prefix} { + set update [linsert $update end-$offset $new] + } else { + lappend update $new + } + } + set auto_path $update + + + } + #else - if auto_path change wasn't just extra entries - leave as user specified + #review. + + set last_auto_path $auto_path + # ------------- dict set epoch pkg current $current_e dict set epoch pkg epochs $current_e [dict create] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } + if {[dict exists $epoch pkg epochs $prev_e indexes]} { - #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + #bring across each previous 'indexes' record *if* searchpath is within zipfs root static filesystem + # and searchpath is still a path below an auto_path entry. + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -710,6 +970,9 @@ tcl::namespace::eval punk::libunknown { } } } + + #---------------------------------------- + #store basic stats for previous epoch instead of all data. set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e indexes]] set index_count 0 dict for {searchpath indexfiles} [dict get $epoch pkg epochs $prev_e indexes] { @@ -718,12 +981,28 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count $index_searchpath_count index_count $index_count] dict unset epoch pkg epochs $prev_e indexes + #---------------------------------------- } else { dict set epoch pkg epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch pkg epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + if {"clearadded" in $args} { + dict set epoch pkg epochs $current_e added [dict create] + } else { + if {$has_zipfs} { + set zroot [zipfs root] + set prev_added [dict get $epoch pkg epochs $prev_e added] + set keep_added [dict filter $prev_added key $zroot*] + #bring across - each lib will have previous epoch number as the value indicating epoch in which it was found + #dict set epoch pkg epochs $current_e added [dict get $epoch pkg epochs $prev_e added] + dict set epoch pkg epochs $current_e added $keep_added + } else { + dict set epoch pkg epochs $current_e added [dict create] + } + } + + #store basic stats for previous epoch + #------------------------------------ set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e added] { @@ -735,37 +1014,31 @@ tcl::namespace::eval punk::libunknown { } dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] dict unset epoch pkg epochs $prev_e added + #------------------------------------ } else { dict set epoch pkg epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch pkg epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch pkg epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch pkg epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch pkg epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch pkg epochs $prev_e unfound - } } errM]} { - puts stderr "epoch_incr_pkg error\n $errM" + puts stderr "epoch_incr_pkg error\n $errM\n$::errorInfo" } } proc epoch_incr_tm {args} { if {[catch { upvar ::punk::libunknown::epoch epoch + dict set epoch scripts {} set prev_e [dict get $epoch tm current] set current_e [expr {$prev_e + 1}] dict set epoch tm current $current_e dict set epoch tm epochs $current_e [dict create] set tmlist [tcl::tm::list] + if {[info commands ::tcl::zipfs::root] ne ""} { + set has_zipfs 1 + } else { + set has_zipfs 0 + } if {[dict exists $epoch tm epochs $prev_e indexes]} { #bring across the previous indexes records if static filesystem (zipfs) - if {[info commands ::tcl::zipfs::root] ne ""} { + if {$has_zipfs} { set zroot [zipfs root] dict for {searchpath indexfiles} [dict get $epoch tm epochs $prev_e indexes] { if {[string match $zroot* $searchpath]} { @@ -795,8 +1068,21 @@ tcl::namespace::eval punk::libunknown { dict set epoch tm epochs $prev_e indexes_history [dict create searchpath_count 0 index_count 0] } if {[dict exists $epoch tm epochs $prev_e added]} { - #bring across - each lib will have previous epoch number - dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + #todo? cycle through non-statics and add pkgs to pkg untracked if we are deleting 'added' records? + if {"clearadded" in $args} { + dict set epoch tm epochs $current_e added [dict create] + } else { + #bring across - each lib will have previous epoch number + #dict set epoch tm epochs $current_e added [dict get $epoch tm epochs $prev_e added] + if {$has_zipfs} { + set zroot [zipfs root] + dict set epoch tm epochs $current_e added [dict filter [dict get $epoch tm epochs $prev_e added] key $zroot*] + } else { + dict set epoch tm epochs $current_e added [dict create] + } + } + + set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e added]] set lib_count 0 dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e added] { @@ -811,26 +1097,77 @@ tcl::namespace::eval punk::libunknown { } else { dict set epoch tm epochs $prev_e added_history [dict create searchpath_count 0 lib_count 0] } - if {[dict exists $epoch tm epochs $prev_e unfound]} { - set index_searchpath_count [dict size [dict get $epoch tm epochs $prev_e unfound]] - set lib_count 0 - dict for {searchpath libinfo} [dict get $epoch tm epochs $prev_e unfound] { - dict for {lib e} $libinfo { - if {$e == $prev_e} { - incr lib_count - } - } - } - dict set epoch tm epochs $prev_e unfound_history [dict create searchpath_count $index_searchpath_count lib_count $lib_count] - dict unset epoch tm epochs $prev_e unfound - } } errM]} { puts stderr "epoch_incr_tm error\n $errM" } } - proc init {} { + #see what basic info we can gather *quickly* about the indexes for each version of a pkg that the package db knows about. + #we want no calls out to the actual filesystem - but we can use some 'file' calls such as 'file dirname', 'file split' (review -safe interp problem) + #in practice the info is only available for tm modules + proc packagedb_indexinfo {pkg} { + if {[string match ::* $pkg]} { + error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'" + } + set versions [lsort -command {package vcompare} [package versions $pkg]] + if {[llength $versions] == 0} { + set v [package provide $pkg] + } + set versionlist [list] + foreach v $versions { + set ifneededscript [package ifneeded $pkg $v] + if {[string trim $ifneededscript] eq ""} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + set scriptlines [split $ifneededscript \n] + if {[llength $scriptlines] > 1} { + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[catch {llength $ifneededscript}]} { + #scripts aren't necessarily 'list shaped' - we don't want to get into the weeds trying to make sense of arbitrary scripts. + lappend versionlist [list $v type unknown index "" indexbase ""] + continue + } + if {[lindex $ifneededscript 0] eq "package" && [lindex $ifneededscript 1] eq "provide" && [file extension [lindex $ifneededscript end]] eq ".tm"} { + set tmfile [lindex $ifneededscript end] + set nspath [namespace qualifiers $pkg] + if {$nspath eq ""} { + set base [file dirname $tmfile] + } else { + set nsparts [string map {:: " "} $nspath] ;#*naive* split - we are assuming (fairly reasonably) there are no namespaces containing spaces for a .tm module + set pathparts [file split [file dirname $tmfile]] + set baseparts [lrange $pathparts 0 end-[llength $nsparts]] + set base [file join {*}$baseparts] + } + lappend versionlist [list $v type tm index $tmfile indexbase $base script $ifneededscript] + } else { + #we could guess at the pkgindex.tcl file used based on simple pkg ifneeded scripts .tcl path compared to ::auto_index + #but without hitting filesystem to verify - it's unsatisfactory + lappend versionlist [list $v type unknown index "" indexbase "" script $ifneededscript] + } + } + return $versionlist + } + proc init {args} { + variable last_auto_path + set last_auto_path [set ::auto_path] + variable last_tm_paths + set last_tm_paths [set ::tcl::tm::paths] + + set callerposn [lsearch $args -caller] + if {$callerposn > -1} { + set caller [lindex $args $callerposn+1] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - caller:$caller\x1b\[m" + #puts stderr "punk::libunknown::init auto_path : $::auto_path" + #puts stderr "punk::libunknown::init tcl::tm::list: [tcl::tm::list]" + } + + + + if {[catch {tcl::tm::list} tmlist]} { set tmlist [list] } @@ -850,10 +1187,113 @@ tcl::namespace::eval punk::libunknown { #This is far from conclusive - there may be other renamers (e.g commandstack) return } + + + if {[info commands ::punk::libunknown::package] ne ""} { puts stderr "punk::libunknown::init already done - unnecessary call? info frame -1: [info frame -1]" return } + variable epoch + if {![info exists epoch]} { + set tmstate [dict create 0 {added {}}] + set pkgstate [dict create 0 {added {}}] + set tminfo [dict create current 0 epochs $tmstate untracked [dict create]] + set pkginfo [dict create current 0 epochs $pkgstate untracked [dict create]] + + set epoch [dict create scripts {} tm $tminfo pkg $pkginfo] + + #untracked: package names at time of punk::libunknown::init call - or passed with epoch when sharing epoch to another interp. + #The epoch state will need to be incremented and cleared of any 'added' records if any of these are requested during a package unknown call + #Because they were loaded prior to us tracking the epochs - and without trying to examine the ifneeded scripts we don't know the exact paths + #which were scanned to load them. Our 'added' key entries will not contain them because they weren't unknown + } else { + #we're accepting a pre-provided 'epoch' record (probably from another interp) + #the tm untracked and pkg untracked dicts indicate for which packages the pkg added, tm added etc data are not conclusive + #test + #todo? + } + #upon first libunknown::init in the interp, we need to add any of this interp's already known packages to the (possibly existing) tm untracked and pkg untracked dicts. + #(unless we can use packagedb_indexinfo to determine what was previously scanned?) + # review - what if the auto_path or tcl::tm::list was changed between initial scan and call of libunknown::init??? + # This is likely a common scenario?!!! + # For now this is a probable flaw in the logic - we need to ensure libunknown::init is done first thing + # or suffer additional scans.. or document ?? + #ideally init should be called in each interp before any scans for packages so that the list of untracked is minimized. + set pkgnames [package names] + foreach p $pkgnames { + if {[string tolower $p] in {punk::libunknown tcl::zlib tcloo tcl::oo tcl}} { + continue + } + set versions [package versions $p] + if {[llength $versions] == 0} { + continue + } + set versionlist [packagedb_indexinfo $p] + if {[llength $versionlist] == 0} { + continue + } else { + foreach vdata $versionlist { + #dict set epoch scripts $p [lindex $vdata 0] [package ifneeded $p [lindex $vdata 0]] + dict set epoch scripts $p [lindex $vdata 0] [lindex $vdata 8]] + } + if {[lsearch -index 6 $versionlist ""] > -1} { + #There exists at least one empty indexbase for this package - we have to treat it as untracked + dict set epoch tm untracked $p "" ;#value unimportant + dict set epoch pkg untracked $p "" ;#value unimportant + } else { + #update the epoch info with where the tm versions came from + #(not tracking version numbers in epoch - just package to the indexbase) + foreach vdata $versionlist { + lassign $vdata v _t type _index index _indexbase indexbase _script iscript + if {$type eq "tm"} { + if {![dict exists $epoch tm epochs 0 added $indexbase]} { + #dict set epoch tm epochs 0 added $indexbase [dict create $p [dict create e 0 v $v]] + dict set epoch tm epochs 0 added $indexbase $p $v [dict create e 0 scr $iscript] + } else { + set idxadded [dict get $epoch tm epochs 0 added $indexbase] + #dict set idxadded $p [dict create e 0 v $v] + dict set idxadded $p $v [dict create e 0 scr $iscript] + dict set epoch tm epochs 0 added $indexbase $idxadded + } + dict unset epoch tm untracked $p + } elseif {$type eq "pkg"} { + #todo? tcl doesn't give us good introspection on package indexes for packages + #dict unset epoch pkg untracked $p + } + } + } + } + } + + + + + #------------------------------------------------------------- + #set all_untracked [dict keys [dict get $epoch untracked]] + #puts stderr "\x1b\[1\;33m punk::libunknown::init - pkg all_untracked:\x1b\[m [dict size [dict get $epoch pkg untracked]]" + #if {[dict exists $epoch pkg untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in pkg untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in pkg untracked \x1b\[m " + #} + #puts stderr "\x1b\[1\;33m punk::libunknown::init - tm all_untracked:\x1b\[m [dict size [dict get $epoch tm untracked]]" + #if {[dict exists $epoch tm untracked msgcat]} { + # puts stderr "\x1b\[1\;32m punk::libunknown::init msgcat found in tm untracked \x1b\[m " + # set versions [package versions msgcat] + # puts stderr "versions: $versions" + # foreach v $versions { + # puts stdout "v $v ifneeded: [package ifneeded msgcat $v]" + # } + #} else { + # puts stderr "\x1b\[1\;31m punk::libunknown::init msgcat NOT found in tm untracked \x1b\[m " + #} + #------------------------------------------------------------- trace add variable ::auto_path write ::punk::libunknown::epoch_incr_pkg trace add variable ::tcl::tm::paths write ::punk::libunknown::epoch_incr_tm @@ -870,6 +1310,7 @@ tcl::namespace::eval punk::libunknown { #forgetting Tcl or tcl seems to be a bad idea - package require doesn't work afterwards (independent of this pkg) set forgets_requested [lrange $args 1 end] set ok_forgets [list] + upvar ::punk::libunknown::epoch epoch foreach p $forgets_requested { #'package files' not avail in early 8.6 #There can be other custom 'package ifneeded' scripts that don't use source - but still need to be forgotten. @@ -880,7 +1321,7 @@ tcl::namespace::eval punk::libunknown { # lappend ok_forgets $p #} #What then? Hardcoded only for now? - if {$p ni {tcl Tcl tcl::oo}} { + if {$p ni {tcl Tcl tcl::oo tk}} { #tcl::oo returns a comment only for its package provide script "# Already present, OK?" # - so we can't use empty 'ifneeded' script as a determinant. set vpresent [package provide $p] @@ -890,11 +1331,13 @@ tcl::namespace::eval punk::libunknown { set ifneededscript [package ifneeded $p $vpresent] if {[string trim $ifneededscript] ne ""} { lappend ok_forgets $p + dict unset epoch scripts $p } } else { #not loaded - but may have registered ifneeded script(s) in the package database #assume ok to forget lappend ok_forgets $p + dict unset epoch scripts $p } } } @@ -1021,7 +1464,9 @@ tcl::namespace::eval punk::libunknown { #} if {![interp issafe]} { + #J2 package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::punk::libunknown::zipfs_tclPkgUnknown} + #package unknown {::punk::libunknown::zipfs_tm_UnknownHandler ::tclPkgUnknown} } } @@ -1030,11 +1475,280 @@ tcl::namespace::eval punk::libunknown { package unknown {::tcl::tm::UnknownHandler ::tclPkgUnknown} } + proc package_query {pkgname} { + variable epoch + + if {[dict exists $epoch tm untracked $pkgname]} { + set pkg_info "$pkgname tm UNTRACKED" + } else { + set pkg_info "$pkgname not in tm untracked" + } + if {[dict exists $epoch pkg untracked $pkgname]} { + append pkg_info \n "$pkgname pkg UNTRACKED" + } else { + append pkg_info \n "$pkgname not in pkg untracked" + } + + set pkg_epoch [dict get $epoch pkg current] + #set epoch_info [dict get $epoch pkg epochs $pkg_epoch] + #pkg entries are processed by package unknown in reverse - so their order of creaation is opposite to ::auto_path + set r_added [dict create] + foreach path [lreverse [dict keys [dict get $epoch pkg epochs $pkg_epoch added]]] { + dict set r_added $path [dict get $epoch pkg epochs $pkg_epoch added $path] + } + + #set pkg_added [punk::lib::showdict $r_added */$pkgname] + #set added [textblock::frame -title $title $pkg_added] + set rows [list] + dict for {path pkgs} $r_added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + set scriptlen [string length [dict get $data scr]] + append c3 "$v epoch[dict get $data e] ifneededchars:$scriptlen" \n + } + } + set r [list $path $c2 $c3] + lappend rows $r + } + set title "[punk::ansi::a+ green] PKG epoch $pkg_epoch - added [punk::ansi::a]" + set added [textblock::table -title $title -headers [list Path Pkgcount $pkgname] -rows $rows] + + + set pkg_row $added + + set tm_epoch [dict get $epoch tm current] + #set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + set added [dict get $epoch tm epochs $tm_epoch added] + set rows [list] + dict for {path pkgs} $added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + append c3 "$v $data" \n + } + } + set r [list $c1 $c2 $c3] + lappend rows $r + } + set title "TM epoch $tm_epoch - added" + #set added [textblock::frame -title $title $tm_added] + set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows] + + set tm_row $added + + return $pkg_info\n$pkg_row\n$tm_row + } + #*** !doctools #[list_end] [comment {--- end definitions namespace punk::libunknown ---}] } # == === === === === === === === === === === === === === === +namespace eval punk::libunknown { + #for 8.6 compat + if {"::ledit" ni [info commands ::ledit]} { + #maint: taken from punk::lib + proc ledit {lvar first last args} { + upvar $lvar l + #use lindex_resolve to support for example: ledit lst end+1 end+1 h i + set fidx [lindex_resolve [llength $l] $first] + switch -exact -- $fidx { + -3 { + #index below lower bound + set pre [list] + set fidx -1 + } + -2 { + #first index position is greater than index of last element in the list + set pre [lrange $l 0 end] + set fidx [llength $l] + } + default { + set pre [lrange $l 0 $first-1] + } + } + set lidx [lindex_resolve [llength $l] $last] + switch -exact -- $lidx { + -3 { + #index below lower bound + set post [lrange $l 0 end] + } + -2 { + #index above upper bound + set post [list] + } + default { + if {$lidx < $fidx} { + #from ledit man page: + #If last is less than first, then any specified elements will be inserted into the list before the element specified by first with no elements being deleted. + set post [lrange $l $fidx end] + } else { + set post [lrange $l $last+1 end] + } + } + } + set l [list {*}$pre {*}$args {*}$post] + } + + #maint: taken from punk::lib + proc lindex_resolve {len index} { + if {![string is integer -strict $len]} { + #<0 ? + error "lindex_resolve len must be an integer" + } + set index [tcl::string::map {_ {}} $index] ;#forward compatibility with integers such as 1_000 + if {[string is integer -strict $index]} { + #can match +i -i + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } else { + #integer may still have + sign - normalize with expr + return [expr {$index}] + } + } else { + if {[string match end* $index]} { + if {$index ne "end"} { + set op [string index $index 3] + set offset [string range $index 4 end] + if {$op ni {+ -} || ![string is integer -strict $offset]} {error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?"} + if {$op eq "+" && $offset != 0} { + return -2 + } + } else { + #index is 'end' + set index [expr {$len-1}] + if {$index < 0} { + #special case - 'end' with empty list - treat end like a positive number out of bounds + return -2 + } else { + return $index + } + } + if {$offset == 0} { + set index [expr {$len-1}] + if {$index < 0} { + return -2 ;#special case as above + } else { + return $index + } + } else { + #by now, if op = + then offset = 0 so we only need to handle the minus case + set index [expr {($len-1) - $offset}] + } + if {$index < 0} { + return -3 + } else { + return $index + } + } else { + #plain +- already handled above. + #we are trying to avoid evaluating unbraced expr of potentially insecure origin + if {[regexp {(.*)([+-])(.*)} $index _match a op b]} { + if {[string is integer -strict $a] && [string is integer -strict $b]} { + if {$op eq "-"} { + set index [expr {$a - $b}] + } else { + set index [expr {$a + $b}] + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + } else { + error "bad index '$index': must be integer?\[+-\]integer? or end?\[+-\]integer?" + } + if {$index < 0} { + return -3 + } elseif {$index >= $len} { + return -2 + } + return $index + } + } + } + } +} + +tcl::namespace::eval punk::libunknown::lib { + + #A version of textutil::string::longestCommonPrefixList + #(also as ::punk::lib::longestCommonPrefixList) + proc longestCommonPrefix {items} { + if {[llength $items] <= 1} { + return [lindex $items 0] + } + set items [lsort $items[unset items]] + set min [lindex $items 0] + set max [lindex $items end] + #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) + #(sort order nothing to do with length - e.g min may be longer than max) + if {[string length $min] > [string length $max]} { + set temp $min + set min $max + set max $temp + } + set n [string length $min] + set prefix "" + set i -1 + while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { + append prefix $c + } + return $prefix + } + #maint: from punk::lib::ldiff + proc ldiff {fromlist removeitems} { + if {[llength $removeitems] == 0} {return $fromlist} + set result [list] + foreach item $fromlist { + if {$item ni $removeitems} { + lappend result $item + } + } + return $result + } + proc intersect2 {A B} { + #taken from tcl version of struct::set::Intersect + if {[llength $A] == 0} {return {}} + if {[llength $B] == 0} {return {}} + + # This is slower than local vars, but more robust + if {[llength $B] > [llength $A]} { + ::set res $A + ::set A $B + ::set B $res + } + ::set res {} + foreach x $A { + ::set ($x) {} + } + foreach x $B { + if {[info exists ($x)]} { + lappend res $x + } + } + return $res + } + proc is_list_all_in_list {A B} { + if {[llength $A] > [llength $B]} {return 0} + foreach x $B { + ::set ($x) {} + } + foreach x $A { + if {![info exists ($x)]} { + return 0 + } + } + return 1 + } +} # ----------------------------------------------------------------------------- # register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix-0.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix-0.2.tm index 24ef156c..1ac6a836 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix-0.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix-0.2.tm @@ -9,12 +9,12 @@ tcl::namespace::eval punk::mix { package require punk::mix::templates ;#registers as provider pkg for 'punk.templates' capability with punk::cap set t [time { - if {[catch {punk::mix::templates::provider register *} errM]} { - puts stderr "punk::mix failure during punk::mix::templates::provider register *" - puts stderr $errM - puts stderr "-----" - puts stderr $::errorInfo - } + if {[catch {punk::mix::templates::provider register *} errM]} { + puts stderr "punk::mix failure during punk::mix::templates::provider register *" + puts stderr $errM + puts stderr "-----" + puts stderr $::errorInfo + } }] puts stderr "->punk::mix::templates::provider register * t=$t" } diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm index 5e12b9a2..3fb1e001 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/loadedlib-0.1.0.tm @@ -53,11 +53,6 @@ namespace eval punk::mix::commandset::loadedlib { #REVIEW - this doesn't result in full scans catch {package require frobznodule666} ;#ensure pkg system has loaded/searched for everything - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } set packages [package names] set matches [list] foreach search $searchstrings { @@ -85,11 +80,7 @@ namespace eval punk::mix::commandset::loadedlib { # set versions $v #} } - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {$opt_highlight} { set v [package provide $m] if {$v ne ""} { @@ -188,11 +179,6 @@ namespace eval punk::mix::commandset::loadedlib { } proc info {libname} { - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } catch {package require $libname 1-0} ;#ensure pkg system has loaded/searched - using unsatisfiable version range set pkgsknown [package names] if {[set posn [lsearch $pkgsknown $libname]] >= 0} { @@ -201,11 +187,7 @@ namespace eval punk::mix::commandset::loadedlib { puts stderr "Package not found as available library/module - check tcl::tm::list and \$auto_path" } set versions [package versions [lindex $libname 0]] - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {![llength $versions]} { puts stderr "No version numbers found for library/module $libname" return false diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm index 2bc0f01c..723ce06e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/module-0.1.0.tm @@ -77,6 +77,12 @@ namespace eval punk::mix::commandset::module { return $result } #require current dir when calling to be the projectdir, or + punk::args::define { + @dynamic + @id -id "::punk::mix::commandset::module::templates" + @cmd -name "punk::mix::commandset::module::templates" + ${[punk::args::resolved_def -antiglobs {@id @cmd} "::punk::mix::commandset::module::templates_dict"]} + } proc templates {args} { set tdict_low_to_high [templates_dict {*}$args] #convert to screen order - with higher priority at the top @@ -135,16 +141,17 @@ namespace eval punk::mix::commandset::module { globsearches -default * -multiple 1 } proc templates_dict {args} { - set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + #set argd [punk::args::get_by_id ::punk::mix::commandset::module::templates_dict $args] + set argd [punk::args::parse $args withid ::punk::mix::commandset::module::templates_dict] package require punk::cap if {[punk::cap::capability_has_handler punk.templates]} { set template_folder_dict [punk::cap::call_handler punk.templates get_itemdict_moduletemplates {*}$args] } else { put stderr "get_template_basefolders WARNING - no handler available for the 'punk.templates' capability - template providers will be unable to provide template locations" } - } + } + - set moduletypes [punk::mix::cli::lib::module_types] punk::args::define [subst { @id -id ::punk::mix::commandset::module::new @@ -178,7 +185,7 @@ namespace eval punk::mix::commandset::module { set argd [punk::args::get_by_id ::punk::mix::commandset::module::new $args] lassign [dict values $argd] leaders opts values received set module [dict get $values module] - + #set opts [dict merge $defaults $args] #todo - review compatibility between -template and -type diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm index f670c8c0..8abe694e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/project-0.1.0.tm @@ -592,10 +592,23 @@ namespace eval punk::mix::commandset::project { namespace export * namespace path [namespace parent] + punk::args::define { + @id -id ::punk::mix::commandset::project::collection::_default + @cmd -name "punk::mix::commandset::project::collection::_default"\ + -summary\ + "List projects under fossil managment."\ + -help\ + "List projects under fossil management, showing fossil db location and number of checkouts" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * + } #e.g imported as 'projects' - proc _default {{glob {}} args} { + proc _default {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::collection::_default] + set globlist [dict get $argd values glob] + #*** !doctools - #[call [fun _default] [arg glob] [opt {option value...}]] + #[call [fun _default] [arg glob...]] #[para]List projects under fossil management, showing fossil db location and number of checkouts #[para]The glob argument is optional unless option/value pairs are also supplied, in which case * should be explicitly supplied #[para]glob restricts output based on the name of the fossil db file e.g s* for all projects beginning with s @@ -604,7 +617,7 @@ namespace eval punk::mix::commandset::project { #[para] punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection #[para]Will result in the command being available as projects package require overtype - set db_projects [lib::get_projects $glob] + set db_projects [lib::get_projects {*}$globlist] set col1items [lsearch -all -inline -index 0 -subindices $db_projects *] set col2items [lsearch -all -inline -index 1 -subindices $db_projects *] set checkouts [lsearch -all -inline -index 2 -subindices $db_projects *] @@ -1012,12 +1025,21 @@ namespace eval punk::mix::commandset::project { #consider using punk::cap to enable multiple template-substitution providers with their own set of tagnames and/or tag wrappers, where substitution providers are all run return [string cat % $tagname %] } - #get project info only by opening the central confg-db - #(will not have proper project-name etc) - proc get_projects {{globlist {}} args} { - if {![llength $globlist]} { - set globlist [list *] - } + punk::args::define { + @id -id ::punk::mix::commandset::project::lib::get_projects + @cmd -name punk::mix::commandset::project::lib::get_projects\ + -summary\ + "List projects referred to by central fossil config-db."\ + -help\ + "Get project info only by opening the central fossil config-db + (will not have proper project-name etc)" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * -optional 1 + } + proc get_projects {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::lib::get_projects] + set globlist [dict get $argd values glob] + set fossil_prog [auto_execok fossil] set configdb [punk::repo::fossil_get_configdb] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm index e2f44ad3..b40be865 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/nav/fs-0.1.0.tm @@ -940,7 +940,8 @@ tcl::namespace::eval punk::nav::fs { #windows doesn't consider dotfiles as hidden - mac does (?) #we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden if {$::tcl_platform(platform) ne "windows"} { - lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + #lappend flaggedhidden {*}[lsearch -all -inline [list {*}$dirs {*}$files] ".*"] + lappend flaggedhidden {*}[tcl::prefix::all [list {*}$dirs {*}$files] .] #e.g we can have dupes in the case where there are vfs mounted files that appear as dirs #as we will need to do a (nat)sort as a last step - it will be faster to not sort items prematurely #set flaggedhidden [lsort -unique $flaggedhidden] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 0f609b4f..6bd826e2 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -177,10 +177,10 @@ tcl::namespace::eval punk::ns { } else { set fq_nspath $nspath } - if {[catch {nseval_ifexists $fq_nspath {}}]} { - return 0 - } else { + if {[nseval_ifexists $fq_nspath {::string cat ok}] eq "ok"} { return 1 + } else { + return 0 } } @@ -408,6 +408,7 @@ tcl::namespace::eval punk::ns { proc nstail {nspath args} { #normalize the common case of :::: set nspath [string map {:::: ::} $nspath] + #it's unusual - but namespaces *can* have spaced in them. set mapped [string map {:: \u0FFF} $nspath] set parts [split $mapped \u0FFF] @@ -757,13 +758,20 @@ tcl::namespace::eval punk::ns { } set marks [dict create oo \u25c6 ooc \u25c7 ooo \u25c8 punkargs \U1f6c8 ensemble \u24ba native \u24c3 unknown \U2370] if {[llength $ansinames]} { - return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type][punk::ansi::a]" + return "[punk::ansi::a+ {*}$ansinames][dict get $marks $type]\x1b\[0m" } else { return [dict get $marks $type] } } #REVIEW - ansi codes can be *very* confusing to the user when trying to handle lists etc.. + punk::args::define { + @id -id ::punk::ns::get_nslist + @cmd -name punk::ns::get_nslist + @opts + -match -default "" + -nsdict -type dict -default {} + } proc get_nslist {args} { set known_types [list children commands exported imported aliases procs ensembles ooclasses ooobjects ooprivateobjects ooprivateclasses native coroutines interps zlibstreams] set defaults [dict create\ @@ -774,6 +782,9 @@ tcl::namespace::eval punk::ns { set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- set fq_glob [dict get $opts -match] + if {$fq_glob eq ""} { + set fq_glob [uplevel 1 nsthis]::* + } set requested_types [dict get $opts -types] set opt_nsdict [dict get $opts -nsdict] @@ -834,7 +845,7 @@ tcl::namespace::eval punk::ns { set zlibstreams [list] set usageinfo [list] - if {$opt_nsdict eq ""} { + if {![dict size $opt_nsdict]} { set nsmatches [get_ns_dicts $fq_glob -allbelow 0] set itemcount 0 set matches_with_results [list] @@ -866,6 +877,8 @@ tcl::namespace::eval punk::ns { } if {"commands" in $types} { set commands [dict get $contents commands] + } + set usageinfo [dict get $contents usageinfo] foreach t $types { switch -- $t { exported { @@ -909,8 +922,6 @@ tcl::namespace::eval punk::ns { } } } - set usageinfo [dict get $contents usageinfo] - } set numchildren [llength $children] if {$numchildren} { @@ -1067,7 +1078,7 @@ tcl::namespace::eval punk::ns { } else { } if {$cmd in $imported} { - set prefix [overtype::right $prefix "-[a+ yellow bold]I[a+]"] + set prefix [overtype::right $prefix "-[a+ yellow bold]I[a]"] } } if {$cmd in $usageinfo} { @@ -1075,7 +1086,8 @@ tcl::namespace::eval punk::ns { } else { set u "" } - set cmd$i "${prefix} $c$cmd_display$u" + #set cmd$i "${prefix} $c$cmd_display$u" + set cmd$i "${prefix} [punk::ansi::ansiwrap -rawansi $c $cmd_display]$u" #set c$i $c set c$i "" lappend seencmds $cmd @@ -1146,7 +1158,11 @@ tcl::namespace::eval punk::ns { the child namespaces and commands within the namespace(s) matched by glob." @opts - -nspathcommands -type boolean -default 0 + -nspathcommands -type boolean -default 0 -help\ + "When a namespace has entries configured in 'namespace path', the default result for nslist + will display just a basic note: 'Also resolving cmds in namespace paths: '. + If -nspathcommands is true, it will also display subtables showing the commands resolvable + via any such listed namespaces." -types @values -min 0 -max -1 glob -multiple 1 -optional 1 -default "*" @@ -1205,9 +1221,9 @@ tcl::namespace::eval punk::ns { if {[dict size [dict get $nsdict namespacepath]]} { set path_text "" if {!$opt_nspathcommands} { - append path_text \n " also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" + append path_text \n " Also resolving cmds in namespace paths: [dict keys [dict get $nsdict namespacepath]]" } else { - append path_text \n " also resolving cmds in namespace paths:" + append path_text \n " Also resolving cmds in namespace paths:" set nspathdict [dict get $nsdict namespacepath] if {!$has_textblock} { dict for {k v} $nspathdict { @@ -1216,8 +1232,14 @@ tcl::namespace::eval punk::ns { append path_text \n " cmds: $cmds" } } else { + #todo - change to display in column order to be same as main command listing dict for {k v} $nspathdict { - set t [textblock::list_as_table -title $k -columns 6 [lsort [dict get $v commands]]] + set pathcommands [dict get $v commands] + set columns 6 + if {[llength $pathcommands] < 6} { + set columns [llength $v] + } + set t [textblock::list_as_table -title $k -columns $columns [lsort $pathcommands]] append path_text \n $t } } @@ -1423,7 +1445,7 @@ tcl::namespace::eval punk::ns { } } return $matches - }] + }]] } else { lappend matched {*}[tcl::namespace::eval $location [list ::info commands [nsjoin ${location} $p]]] @@ -2397,14 +2419,16 @@ tcl::namespace::eval punk::ns { if {$is_ensembleparam} { #review lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } if {![llength $allchoices]} { #review - only leaders with a defined set of choices are eligible for consideration as a subcommand lappend nextqueryargs $q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 set specargs $queryargs_untested continue } @@ -2420,7 +2444,8 @@ tcl::namespace::eval punk::ns { } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args set resolvelist [list {*}$specid {*}$nextqueryargs {*}$queryargs_untested] @@ -2510,8 +2535,12 @@ tcl::namespace::eval punk::ns { punk::args::define { @id -id ::punk::ns::forms - @cmd -name punk::ns::forms -help\ - "Return names for each form of a command" + @cmd -name punk::ns::forms\ + -summary\ + "List command forms."\ + -help\ + "Return names for each form of a command. + Most commands are single-form and will only return the name '_default'." @opts @values -min 1 -max -1 cmditem -multiple 1 -optional 0 @@ -2523,12 +2552,37 @@ tcl::namespace::eval punk::ns { set id [dict get $cmdinfo origin] ::punk::args::forms $id } + + + punk::args::define { + @id -id ::punk::ns::eg + @cmd -name punk::ns::eg\ + -summary\ + "Return command examples."\ + -help\ + "Return the -help info from the @examples directive + in a command definition." + @values -min 1 -max -1 + cmditem -multiple 1 -optional 0 + } + proc eg {args} { + set argd [::punk::args::parse $args withid ::punk::ns::eg] + set cmdmembers [dict get $argd values cmditem] + set cmdinfo [uplevel 1 [list ::punk::ns::resolve_command {*}$cmdmembers]] ;#resolve from calling context + set resolved_id [dict get $cmdinfo origin] + set result [::punk::args::eg $resolved_id] + } + + punk::args::define { @id -id ::punk::ns::synopsis - @cmd -name punk::ns::synopsis -help\ + @cmd -name punk::ns::synopsis\ + -summary\ + "Return command synopsis."\ + -help\ "Return synopsis for each form of a command on separate lines. - If -form is given, supply only + If -form formname| is given, supply only the synopsis for that form. " @opts @@ -2564,8 +2618,12 @@ tcl::namespace::eval punk::ns { full - summary { set resultstr "" foreach synline [split $syn \n] { - #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n - append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + if {[string range $synline 0 1] eq "# "} { + append resultstr $synline \n + } else { + #append resultstr [join [lreplace $synline 0 0 {*}$idparts] " "] \n + append resultstr [join [lreplace $synline 0 [llength $resolved_id]-1 {*}$idparts] " "] \n + } } set resultstr [string trimright $resultstr \n] #set resultstr [join [lreplace $syn 0 0 {*}$idparts] " "] @@ -2591,7 +2649,10 @@ tcl::namespace::eval punk::ns { punk::args::define { @dynamic @id -id ::punk::ns::arginfo - @cmd -name punk::ns::arginfo -help\ + @cmd -name punk::ns::arginfo\ + -summary\ + "Command usage/help."\ + -help\ "Show usage info for a command. It supports the following: 1) Procedures or builtins for which a punk::args definition has @@ -2618,6 +2679,9 @@ tcl::namespace::eval punk::ns { } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { -form -default 0 -help\ "Ordinal index or name of command form" + -grepstr -default "" -type list -typesynopsis regex -help\ + "list consisting of regex, optionally followed by ANSI names for highlighting + (incomplete - todo)" -- -type none -help\ "End of options marker Use this if the command to view begins with a -" @@ -2642,6 +2706,8 @@ tcl::namespace::eval punk::ns { set querycommand [dict get $values commandpath] set queryargs [dict get $values subcommand] + set grepstr [dict get $opts -grepstr] + set opts [dict remove $opts -grepstr] #puts stdout "---------------------arginfo: '$args' querycommand:'$querycommand' queryargs:'$queryargs'" #todo - similar to corp? review corp resolution process @@ -2905,7 +2971,8 @@ tcl::namespace::eval punk::ns { break } lappend nextqueryargs $resolved_q - lpop queryargs_untested 0 + #lpop queryargs_untested 0 + ledit queryargs_untested 0 0 if {$resolved_q ne $q} { #we have our first difference - recurse with new query args #set numvals [expr {[llength $queryargs]+1}] @@ -3020,8 +3087,11 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} new" - @cmd -name "${$origin} new" -help\ - "create object with specified command name. + @cmd -name "${$origin} new"\ + -summary\ + "Create new object instance."\ + -help\ + "create object with autogenerated command name. Arguments are passed to the constructor." @values }] @@ -3071,7 +3141,10 @@ tcl::namespace::eval punk::ns { set arglist [lindex $constructorinfo 0] set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} create" - @cmd -name "${$origin} create" -help\ + @cmd -name "${$origin} create"\ + -summary\ + "Create new object instance with specified command name."\ + -help\ "create object with specified command name. Arguments following objectName are passed to the constructor." @values -min 1 @@ -3124,7 +3197,10 @@ tcl::namespace::eval punk::ns { # but we may want notes about a specific destructor set argdef [punk::lib::tstr -return string { @id -id "(autodef)${$origin} destroy" - @cmd -name "destroy" -help\ + @cmd -name "destroy"\ + -summary\ + "delete object instance."\ + -help\ "delete object, calling destructor if any. destroy accepts no arguments." @values -min 0 -max 0 @@ -3601,6 +3677,13 @@ tcl::namespace::eval punk::ns { set msg "Undocumented command $origin. Type: $cmdtype" } } + if {[llength $grepstr] != 0} { + if {[llength $grepstr] == 1} { + return [punk::grepstr -no-linenumbers -highlight red [lindex $grepstr 0] $msg] + } else { + return [punk::grepstr -no-linenumbers -highlight [lrange $grepstr 1 end] [lindex $grepstr 0] $msg] + } + } return $msg } @@ -3620,6 +3703,21 @@ tcl::namespace::eval punk::ns { comment inserted to display information such as the namespace origin. Such a comment begins with #corp#." @opts + -syntax -default basic -choices {none basic}\ + -choicelabels { + none\ + " Plain text output" + basic\ + " Comment and bracket highlights. + This is a basic colourizer - not + a full Tcl syntax highlighter." + }\ + -help\ + "Type of syntax highlighting on result. + Note that -syntax none will always return a proper Tcl + List: proc + - but a syntax highlighter may return a string that + is not a Tcl list." @values -min 1 -max -1 commandname -help\ "May be either the fully qualified path for the command, @@ -3628,7 +3726,8 @@ tcl::namespace::eval punk::ns { } proc corp {args} { set argd [punk::args::parse $args withid ::punk::ns::corp] - set path [dict get $argd values commandname] + set path [dict get $argd values commandname] + set syntax [dict get $argd opts -syntax] #thanks to Richard Suchenwirth for the original - wiki.tcl-lang.org/page/corp #Note: modified here to support aliases and relative/absolute name (with respect to namespace .ie ::name vs name) if {[info exists punk::console::tabwidth]} { @@ -3713,7 +3812,19 @@ tcl::namespace::eval punk::ns { lappend argl $a } #list proc [nsjoin ${targetns} $name] $argl $body - list proc $resolved $argl $body + switch -- $syntax { + basic { + #rudimentary colourising only + set argl [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $argl] + set body [punk::grepstr -return all -highlight green {^\s*#.*} $body] ;#Note, will not highlight comments at end of line - like this one. + set body [punk::grepstr -return all -highlight tk-darkcyan {\{|\}} $body] + set body [punk::grepstr -return all -highlight tk-orange {\[|\]} $body] + #ansi colourised items in list format may not always have desired string representation (list escaping can occur) + #return as a string - which may not be a proper Tcl list! + return "proc $resolved {$argl} {\n$body\n}" + } + } + list proc $resolved $argl $body } @@ -3799,13 +3910,53 @@ tcl::namespace::eval punk::ns { } + punk::args::define { + @id -id ::punk::ns::pkguse + @cmd -name punk::ns::pkguse -help\ + "Load package and move to namespace of the same name if run + interactively with only pkg/namespace argument. + if script and args are supplied, the + script runs in the namespace with the args passed to the script. + + todo - further documentation" + @leaders -min 1 -max 1 + pkg_or_existing_ns -type string + @opts + -vars -type none -help\ + "whether to capture namespace vars for use in the supplied script" + -nowarnings -type none + @values -min 0 -max -1 + script -type string -optional 1 + arg -type any -optional 1 -multiple 1 + } #load package and move to namespace of same name if run interactively with only pkg/namespace argument. #if args is supplied - first word is script to run in the namespace remaining args are args passed to scriptblock #if no newline or $args in the script - treat as one-liner and supply {*}$args automatically - proc pkguse {pkg_or_existing_ns args} { - lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs - set use_vars [expr {"-vars" in $runopts}] - set no_warnings [expr {"-nowarnings" in $runopts}] + proc pkguse {args} { + set argd [punk::args::parse $args withid ::punk::ns::pkguse] + lassign [dict values $argd] leaders opts values received + puts stderr "leaders:$leaders opts:$opts values:$values received:$received" + + set pkg_or_existing_ns [dict get $leaders pkg_or_existing_ns] + if {[dict exists $received script]} { + set scriptblock [dict get $values script] + } else { + set scriptblock "" + } + if {[dict exists $received arg]} { + set arglist [dict get $values arg] + } else { + set arglist [list] + } + + set use_vars [dict exists $received "-vars"] + set no_warnings [dict exists $received "-nowarnings"] + + #lassign [internal::get_run_opts {-vars -nowarnings} {} $args] _r runopts _c cmdargs + #set use_vars [expr {"-vars" in $runopts}] + #set no_warnings [expr {"-nowarnings" in $runopts}] + + set ver "" @@ -3883,7 +4034,7 @@ tcl::namespace::eval punk::ns { } } if {[tcl::namespace::exists $ns]} { - if {[llength $cmdargs]} { + if {[dict exists $received script]} { set binding {} #if {[info level] == 1} { # #up 1 is global @@ -3923,7 +4074,7 @@ tcl::namespace::eval punk::ns { } ] - set arglist [lassign $cmdargs scriptblock] + #set arglist [lassign $cmdargs scriptblock] if {[string first "\n" $scriptblock] <0 && [string first {$args} $scriptblock] <0} { #one liner without use of $args append scriptblock { {*}$args} diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm index 317fc9de..dabf7f8e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/packagepreference-0.1.0.tm @@ -110,9 +110,9 @@ tcl::namespace::eval punk::packagepreference { #[list_begin definitions] lappend PUNKARGS [list { - @id -id ::punk::packagepreference::install - @cmd -name ::punk::packagepreference::install -help\ - "Install override for ::package builtin - for 'require' subcommand only." + @id -id ::punk::packagepreference::uninstall + @cmd -name ::punk::packagepreference::uninstall -help\ + "Uninstall override for ::package builtin - for 'require' subcommand only." @values -min 0 -max 0 }] proc uninstall {} { @@ -194,7 +194,7 @@ tcl::namespace::eval punk::packagepreference { if {!$is_exact && [llength $vwant] <= 1 } { #required version unspecified - or specified singularly set available_versions [$COMMANDSTACKNEXT_ORIGINAL versions $pkg] - if {[llength $available_versions] > 1} { + if {[llength $available_versions] >= 1} { # --------------------------------------------------------------- #An attempt to detect dll/so loaded and try to load same version #dll/so files are often named with version numbers that don't contain dots or a version number at all @@ -202,9 +202,11 @@ tcl::namespace::eval punk::packagepreference { set pkgloadedinfo [lsearch -nocase -inline -index 1 [info loaded] $pkg] if {[llength $pkgloadedinfo]} { - puts stderr "--> pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and multiple versions available" - lassign $pkgloadedinfo path name - set lcpath [string tolower $path] + if {[llength $available_versions] > 1} { + puts stderr "--> pkg $pkg not already 'provided' but shared object seems to be loaded: $pkgloadedinfo - and [llength $available_versions] versions available" + } + lassign $pkgloadedinfo loaded_path name + set lc_loadedpath [string tolower $loaded_path] #first attempt to find a match for our loaded sharedlib path in a *simple* package ifneeded statement. set lcpath_to_version [dict create] foreach av $available_versions { @@ -212,17 +214,19 @@ tcl::namespace::eval punk::packagepreference { #ifneeded script not always a valid tcl list if {![catch {llength $scr} scrlen]} { if {$scrlen == 3 && [lindex $scr 0] eq "load" && [string match -nocase [lindex $scr 2] $pkg]} { + #a basic 'load ' statement dict set lcpath_to_version [string tolower [lindex $scr 1]] $av } } } - if {[dict exists $lcpath_to_version $lcpath]} { - set lversion [dict get $lcpath_to_version $lcpath] + if {[dict exists $lcpath_to_version $lc_loadedpath]} { + set lversion [dict get $lcpath_to_version $lc_loadedpath] } else { #fallback to a best effort guess based on the path - set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $path $pkg] + set lversion [::punk::packagepreference::system::slibpath_guess_pkgversion $loaded_path $pkg] } + #puts "====lcpath_to_version: $lcpath_to_version" if {$lversion ne ""} { #name matches pkg #hack for known dll version mismatch @@ -232,24 +236,103 @@ tcl::namespace::eval punk::packagepreference { if {[llength $vwant] == 1} { #todo - still check vsatisfies - report a conflict? review } - return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + #return [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + try { + set result [$COMMANDSTACKNEXT require $pkg $lversion-$lversion] + } trap {} {emsg eopts} { + #REVIEW - this occurred in punkmagic (rebuild of tclmagic) - probably due to multiple versions of registry + #under different auto_path folders - and mal-ordering in punk::libunknown's tclPkgUnknown + #May be obsolete.. issue still not clear + + #A hack for 'couldn't open "": permission denied' + #This happens for example with the tcl9registry13.dll when loading from zipfs - but not in all systems, and not for all dlls. + #exact cause unknown. + #e.g + #%package ifneeded registry 1.3.7 + #- load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #%load //zipfs:/app/lib_tcl9/registry1.3/tcl9registry13.dll Registry + #couldn't open "C:/Users/sleek/AppData/Local/Temp/TCL00003cf8/tcl9registry13.dll": permission denied + + #a subsequent load of the path used in the error message works. + + #if {[string match "couldn't open \"*\": permission denied" $emsg]} {} + if {[regexp {couldn't open "(.*)":.*permission denied.*} $emsg _ newpath]} { + #Since this is a hack that shouldn't be required - be noisy about it. + puts stderr ">>> $emsg" + puts stderr "punk::packagepreference::require hack: Re-trying load of $pkg with path: $newpath" + return [load $newpath $pkg] + } else { + #puts stderr "??? $emsg" + #dunno - re-raise + return -options $eopts $emsg + } + } + return $result } + #else puts stderr "> no version determined for pkg: $pkg loaded_path: $loaded_path" } } } # --------------------------------------------------------------- - set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + #?? + #set pkgloadedinfo [lsearch -inline -index 1 [info loaded] $pkg] + + if {[regexp {[A-Z]} $pkg]} { + #legacy package names #only apply catch & retry if there was a cap - otherwise we'll double try for errors unrelated to capitalisation if {[catch {$COMMANDSTACKNEXT require [string tolower $pkg] {*}$vwant} v]} { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } } else { - return $v + set require_result $v } } else { - return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + #return [$COMMANDSTACKNEXT require $pkg {*}$vwant] + try { + set require_result [$COMMANDSTACKNEXT require $pkg {*}$vwant] + } trap {} {emsg eopts} { + return -options $eopts $emsg + } + } + #--------------------------------------------------------------- + #load relevant punk::args:: package(s) + #todo - review whether 'packagepreference' is the right place for this. + #It is conceptually different from the main functions of packagepreference, + #but we don't really want to have a chain of 'package' overrides slowing performance. + #there may be a more generic way to add soft side-dependencies that the original package doesn't/can't specify. + #--------------------------------------------------------------- + + set lc_pkg [string tolower $pkg] + #todo - lookup list of docpkgs for a package? from where? + #we should have the option to not load punk::args:: at all for many(most?) cases where they're unneeded. + #e.g skip if not ::tcl_interactive? + switch -exact -- $lc_pkg { + tcl { + set docpkgs [list tclcore] + } + tk { + set docpkgs [list tkcore] + } + default { + set docpkgs [list $lc_pkg] + } + } + foreach dp $docpkgs { + #review - versions? + #we should be able to load more specific punk::args pkg based on result of [package present $pkg] + catch { + #$COMMANDSTACKNEXT require $pkg {*}$vwant + #j2 + $COMMANDSTACKNEXT require punk::args::$dp + } } + #--------------------------------------------------------------- + return $require_result } default { return [$COMMANDSTACKNEXT {*}$args] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm index ff48fcb0..54ee4080 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/path-0.1.0.tm @@ -325,7 +325,8 @@ namespace eval punk::path { lappend finalparts .. } default { - lpop finalparts + #lpop finalparts + ledit finalparts end end } } } @@ -345,7 +346,8 @@ namespace eval punk::path { switch -exact -- $p { . - "" {} .. { - lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + #lpop finalparts ;#uses punk::lib::compat::lpop if on < 8.7 + ledit finalparts end end ;#uses punk::lib::compat::ledit if on < 8.7 } default { lappend finalparts $p diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/pipe-1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/pipe-1.0.tm index 0b5501ac..2b0500b8 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/pipe-1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/pipe-1.0.tm @@ -373,6 +373,7 @@ tcl::namespace::eval punk::pipe::lib { if {$end_var_posn > 0} { #tcl scan with %s will not handle whitespace as desired. Be explicit using string range instead. #lassign [scan $token %${end_var_posn}s%s] var spec + #lassign [punk::lib::string_splitbefore $token $end_var_posn] var spec set var [string range $token 0 $end_var_posn-1] set spec [string range $token $end_var_posn end] ;#key section includes the terminal char which ended the var and starts the spec } else { @@ -430,7 +431,7 @@ tcl::namespace::eval punk::pipe::lib { } #if {[string length $token]} { - # #lappend varlist [splitstrposn $token $end_var_posn] + # #lappend varlist [punk::lib::string_splitbefore $token $end_var_posn] # set var $token # set spec "" # if {$end_var_posn > 0} { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm index 7bf8306e..b060ab4d 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/repl/codethread-0.1.1.tm @@ -116,7 +116,7 @@ tcl::namespace::eval punk::repl::codethread { #review/test catch {package require punk::ns} - catch {package rquire punk::repl} + catch {package require punk::repl} #variable xyz diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm index 96350c0b..97bbe591 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -420,7 +420,11 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::Addentry - @cmd -name punk::zip::Addentry -help "Add a single file at 'path' to open channel 'zipchan' + @cmd -name punk::zip::Addentry\ + -summary\ + "Add zip-entry for file at 'path'"\ + -help\ + "Add a single file at 'path' to open channel 'zipchan' return a central directory file record" @opts -comment -default "" -help "An optional comment specific to the added file" @@ -543,7 +547,7 @@ tcl::namespace::eval punk::zip { puts -nonewline $zipchan $ddesc } } - + #PK\x01\x02 Cdentral directory file header #set v1 0x0317 ;#upper byte 03 -> UNIX lower byte 23 -> 2.3 set v1 0x0017 ;#upper byte 00 -> MS_DOS and OS/2 (FAT/VFAT/FAT32 file systems) @@ -565,7 +569,10 @@ tcl::namespace::eval punk::zip { punk::args::define { @id -id ::punk::zip::mkzip @cmd -name punk::zip::mkzip\ - -help "Create a zip archive in 'filename'" + -summary\ + "Create a zip archive in 'filename'."\ + -help\ + "Create a zip archive in 'filename'" @opts -offsettype -default "archive" -choices {archive file}\ -help "zip offsets stored relative to start of entire file or relative to start of zip-archive diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm index a4113c45..50bcc2f8 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkcheck-0.1.0.tm @@ -243,14 +243,10 @@ namespace eval punkcheck { } method get_targets_exist {} { set punkcheck_folder [file dirname [$o_installer get_checkfile]] + #puts stdout "### punkcheck glob -dir $punkcheck_folder -tails {*}$o_targets" + #targets can be paths such as punk/mix/commandset/module-0.1.0.tm - glob can search levels below supplied -dir set existing [glob -nocomplain -dir $punkcheck_folder -tails {*}$o_targets] - #set existing [list] - #foreach t $o_targets { - # if {[file exists [file join $punkcheck_folder $t]]} { - # lappend existing $t - # } - #} return $existing } method end {} { diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.tm new file mode 100644 index 00000000..61120a63 --- /dev/null +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/shellfilter-0.2.tm @@ -0,0 +1,3329 @@ +#copyright 2023 Julian Marcel Noble +#license: BSD (revised 3-clause) +# +#Note shellfilter is currently only directly useful for unidirectional channels e.g stdin,stderr,stdout, or for example fifo2 where only one direction is being used. +#To generalize this to bidrectional channels would require shifting around read & write methods on transform objects in a very complicated manner. +#e.g each transform would probably be a generic transform container which holds sub-objects to which read & write are indirected. +#This is left as a future exercise...possibly it's best left as a concept for uni-directional channels anyway +# - as presumably the reads/writes from a bidirectional channel could be diverted off to unidirectional pipelines for processing with less work +# (and maybe even better speed/efficiency if the data volume is asymmetrical and there is significant processing on one direction) +# + + +tcl::namespace::eval shellfilter::log { + variable allow_adhoc_tags 1 + variable open_logs [tcl::dict::create] + variable is_enabled 0 + + proc disable {} { + variable is_enabled + set is_enabled 0 + proc ::shellfilter::log::open {tag settingsdict} {} + proc ::shellfilter::log::write {tag msg} {} + proc ::shellfilter::log::write_sync {tag msg} {} + proc ::shellfilter::log::close {tag} {} + } + + proc enable {} { + variable is_enabled + set is_enabled 1 + #'tag' is an identifier for the log source. + # each tag will use it's own thread to write to the configured log target + proc ::shellfilter::log::open {tag {settingsdict {}}} { + upvar ::shellfilter::sources sourcelist + if {![dict exists $settingsdict -tag]} { + tcl::dict::set settingsdict -tag $tag + } else { + #review + if {$tag ne [tcl::dict::get $settingsdict -tag]} { + error "shellfilter::log::open first argument tag: '$tag' does not match -tag '[tcl::dict::get $settingsdict -tag]' omit -tag, or supply same value" + } + } + if {$tag ni $sourcelist} { + lappend sourcelist $tag + } + + #note new_worker + set worker_tid [shellthread::manager::new_worker $tag $settingsdict] + #puts stderr "shellfilter::log::open this_threadid: [thread::id] tag: $tag worker_tid: $worker_tid" + return $worker_tid + } + proc ::shellfilter::log::write {tag msg} { + upvar ::shellfilter::sources sourcelist + variable allow_adhoc_tags + if {!$allow_adhoc_tags} { + if {$tag ni $sourcelist} { + error "shellfilter::log::write tag '$tag' hasn't been initialised with a call to shellfilter::log::open $tag , and allow_adhoc_tags has been set false. use shellfilter::log::require_open false to allow adhoc tags" + } + } + shellthread::manager::write_log $tag $msg + } + #write_sync - synchronous processing with logging thread, slower but potentially useful for debugging/testing or forcing delay til log written + proc ::shellfilter::log::write_sync {tag msg} { + shellthread::manager::write_log $tag $msg -async 0 + } + proc ::shellfilter::log::close {tag} { + #shellthread::manager::close_worker $tag + shellthread::manager::unsubscribe [list $tag]; #workertid will be added back to free list if no tags remain subscribed + } + + } + + #review + #configure whether we can call shellfilter::log::write without having called open first + proc require_open {{is_open_required {}}} { + variable allow_adhoc_tags + if {![string length $is_open_required]} { + return $allow_adhoc_tags + } else { + set truevalues [list y yes true 1] + set falsevalues [list n no false 0] + if {[string tolower $is_open_required] in $truevalues} { + set allow_adhoc_tags 1 + } elseif {[string tolower $is_open_required] in $falsevalues} { + set allow_adhoc_tags 0 + } else { + error "shellfilter::log::require_open unrecognised value '$is_open_required' try one of $truevalues or $falsevalues" + } + } + } + if {[catch {package require shellthread}]} { + shellfilter::log::disable + } else { + shellfilter::log::enable + } + +} +namespace eval shellfilter::pipe { + #write channel for program. workerthread reads other end of fifo2 and writes data somewhere + proc open_out {tag_pipename {pipesettingsdict {}}} { + set defaultsettings {-buffering full} + set settingsdict [dict merge $defaultsettings $pipesettingsdict] + package require shellthread + #we are only using the fifo in a single direction to pipe to another thread + # - so whilst wchan and rchan could theoretically each be both read & write we're only using them for one operation each + if {![catch {package require Memchan}]} { + lassign [fifo2] wchan rchan + } else { + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + } + #default -translation for both types of fifo on windows is {auto crlf} + # -encoding is as per '[encoding system]' on the platform - e.g utf-8 (e.g windows when beta-utf8 enabled) + chan configure $wchan -buffering [dict get $settingsdict -buffering] ;# + #application end must not be binary for our filters to operate on it + + + #chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation binary ;#works reasonably.. + chan configure $rchan -buffering [dict get $settingsdict -buffering] -translation lf + + set worker_tid [shellthread::manager::new_pipe_worker $tag_pipename $settingsdict] + #puts stderr "worker_tid: $worker_tid" + + #set_read_pipe does the thread::transfer of the rchan end. -buffering setting is maintained during thread transfer + shellthread::manager::set_pipe_read_from_client $tag_pipename $worker_tid $rchan + + set pipeinfo [list localchan $wchan remotechan $rchan workertid $worker_tid direction out] + return $pipeinfo + } + + #read channel for program. workerthread writes to other end of fifo2 from whereever it's reading (stdin, file?) + proc open_in {tag_pipename {settingsdict {} }} { + package require shellthread + package require tcl::chan::fifo2 + lassign [tcl::chan::fifo2] wchan rchan + set program_chan $rchan + set worker_chan $wchan + chan configure $worker_chan -buffering [dict get $settingsdict -buffering] + chan configure $program_chan -buffering [dict get $settingsdict -buffering] + + chan configure $program_chan -blocking 0 + chan configure $worker_chan -blocking 0 + set worker_tid [shellthread::manager::new_worker $tag_pipename $settingsdict] + + shellthread::manager::set_pipe_write_to_client $tag_pipename $worker_tid $worker_chan + + set pipeinfo [list localchan $program_chan remotechan $worker_chan workertid $worker_tid direction in] + puts stderr "|jn>pipe::open_in returning $pipeinfo" + puts stderr "program_chan: [chan conf $program_chan]" + return $pipeinfo + } + +} + + + +namespace eval shellfilter::ansi { + #maint warning - + #ansistrip from punk::ansi is better/more comprehensive + proc stripcodes {text} { + #obsolete? + #single "final byte" in the range 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~). + dict set escape_terminals CSI [list @ \\ ^ _ ` | ~ a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "\{" "\}"] + #dict set escape_terminals CSI [list J K m n A B C D E F G s u] ;#basic + dict set escape_terminals OSC [list \007 \033\\] ;#note mix of 1 and 2-byte terminals + #we process char by char - line-endings whether \r\n or \n should be processed as per any other character. + #line endings can theoretically occur within an ansi escape sequence (review e.g title?) + set inputlist [split $text ""] + set outputlist [list] + + #self-contained 2 byte ansi escape sequences - review more? + set 2bytecodes_dict [dict create\ + "reset_terminal" "\033c"\ + "save_cursor_posn" "\u001b7"\ + "restore_cursor_posn" "\u001b8"\ + "cursor_up_one" "\u001bM"\ + ] + set 2bytecodes [dict values $2bytecodes_dict] + + set in_escapesequence 0 + #assumption - undertext already 'rendered' - ie no backspaces or carriagereturns or other cursor movement controls + set i 0 + foreach u $inputlist { + set v [lindex $inputlist $i+1] + set uv ${u}${v} + if {$in_escapesequence eq "2b"} { + #2nd byte - done. + set in_escapesequence 0 + } elseif {$in_escapesequence != 0} { + set escseq [dict get $escape_terminals $in_escapesequence] + if {$u in $escseq} { + set in_escapesequence 0 + } elseif {$uv in $escseq} { + set in_escapseequence 2b ;#flag next byte as last in sequence + } + } else { + #handle both 7-bit and 8-bit CSI and OSC + if {[regexp {^(?:\033\[|\u009b)} $uv]} { + set in_escapesequence CSI + } elseif {[regexp {^(?:\033\]|\u009c)} $uv]} { + set in_escapesequence OSC + } elseif {$uv in $2bytecodes} { + #self-contained e.g terminal reset - don't pass through. + set in_escapesequence 2b + } else { + lappend outputlist $u + } + } + incr i + } + return [join $outputlist ""] + } + +} +namespace eval shellfilter::chan { + set testobj ::shellfilter::chan::var + if {$testobj ni [info commands $testobj]} { + + oo::class create var { + variable o_datavar + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + set varname [dict get $settingsdict -varname] + set o_datavar $varname + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 1 ;# as a var is diversionary - default it to be a jucntion + } + } + method initialize {ch mode} { + return [list initialize finalize write] + } + method finalize {ch} { + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + method write {ch bytes} { + set stringdata [encoding convertfrom $o_enc $bytes] + append $o_datavar $stringdata + return "" + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line full none] + } + } + + #todo - something similar for multiple grep specs each with own -pre & -post .. store to dict? + oo::class create tee_grep_to_var { + variable o_datavar + variable o_lastxlines + variable o_trecord + variable o_grepfor + variable o_prelines + variable o_postlines + variable o_postcountdown + variable o_enc + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_lastxlines [list] + set o_postcountdown 0 + set defaults [tcl::dict::create -pre 1 -post 1] + set settingsdict [tcl::dict::get $tf -settings] + set settings [tcl::dict::merge $defaults $settingsdict] + set o_datavar [tcl::dict::get $settings -varname] + set o_grepfor [tcl::dict::get $settings -grep] + set o_prelines [tcl::dict::get $settings -pre] + set o_postlines [tcl::dict::get $settings -post] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set lastx $o_lastxlines + lappend o_lastxlines $logdata + + if {$o_postcountdown > 0} { + append $o_datavar $logdata + if {[regexp $o_grepfor $logdata]} { + #another match in postlines + set o_postcountdown $o_postlines + } else { + incr o_postcountdown -1 + } + } else { + if {[regexp $o_grepfor $logdata]} { + append $o_datavar [join $lastx] + append $o_datavar $logdata + set o_postcountdown $o_postlines + } + } + + if {[llength $o_lastxlines] > $o_prelines} { + set o_lastxlines [lrange $o_lastxlines 1 end] + } + return $bytes + } + method meta_is_redirection {} { + return $o_is_junction + } + method meta_buffering_supported {} { + return [list line] + } + } + + oo::class create tee_to_var { + variable o_datavars + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + set varname [tcl::dict::get $settingsdict -varname] + set o_datavars $varname + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize finalize write flush clear] + } + method finalize {ch} { + my destroy + } + method clear {ch} { + return + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {ch count} { + # return ? + #} + #method flush {ch} { + # return "" + #} + method flush {transform_handle} { + #puts stdout "" + #review - just clear o_encbuf and emit nothing? + #we wouldn't have a value there if it was convertable from the channel encoding? + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #test with set x [string repeat " \U1f6c8" 2043] + #or + #test with set x [string repeat " \U1f6c8" 683] + #most windows terminals (at least) may emit two unrecognised chars "??" at the end + + #Our goal with the while loop here is to avoid encoding conversion errors + #the source of the bogus chars in terminals is unclear. + #Alacritty on windows doesn't seem to have the problem, but wezterm,cmd,windows terminal do. + + #set stringdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + + foreach v $o_datavars { + append $v $stringdata + } + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + oo::class create tee_to_pipe { + variable o_logsource + variable o_localchan + variable o_enc + variable o_encbuf + variable o_trecord + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "tee_to_pipe constructor settingsdict missing -tag" + } + set o_localchan [tcl::dict::get $settingsdict -pipechan] + set o_logsource [tcl::dict::get $settingsdict -tag] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read drain write flush clear finalize] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + method clear {transform_handle} { + return + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {transform_handle bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + #::shellfilter::log::write $o_logsource $logdata + puts -nonewline $o_localchan $stringdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + #a tee is not a redirection - because data still flows along the main path + method meta_is_redirection {} { + return $o_is_junction + } + + } + oo::class create tee_to_log { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set o_encbuf "" + set settingsdict [tcl::dict::get $tf -settings] + if {![tcl::dict::exists $settingsdict -tag]} { + error "tee_to_log constructor settingsdict missing -tag" + } + set o_logsource [tcl::dict::get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {ch mode} { + return [list initialize read write flush finalize] + } + method finalize {ch} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {ch events} { + # must be present but we ignore it because we do not + # post any events + } + method read {ch bytes} { + set logdata [tcl::encoding::convertfrom $o_enc $bytes] + ::shellfilter::log::write $o_logsource $logdata + return $bytes + } + method flush {transform_handle} { + #return "" + set clear $o_encbuf + set o_encbuf "" + return $o_encbuf + } + method write {ch bytes} { + #set logdata [tcl::encoding::convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + ::shellfilter::log::write $o_logsource $logdata + #return $bytes + return [string range $inputbytes 0 end-$tail_offset] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + + oo::class create logonly { + variable o_tid + variable o_logsource + variable o_trecord + variable o_enc + variable o_encbuf + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set o_encbuf "" + set settingsdict [dict get $tf -settings] + if {![dict exists $settingsdict -tag]} { + error "logonly constructor settingsdict missing -tag" + } + set o_logsource [dict get $settingsdict -tag] + set o_tid [::shellfilter::log::open $o_logsource $settingsdict] + } + method initialize {transform_handle mode} { + return [list initialize finalize write] + } + method finalize {transform_handle} { + ::shellfilter::log::close $o_logsource + my destroy + } + method watch {transform_handle events} { + # must be present but we ignore it because we do not + # post any events + } + #method read {transform_handle count} { + # return ? + #} + method write {transform_handle bytes} { + #set logdata [encoding convertfrom $o_enc $bytes] + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return + } + } + + #::shellfilter::log::write_sync $o_logsource $logdata + ::shellfilter::log::write $o_logsource $stringdata + return + } + method meta_is_redirection {} { + return 1 + } + } + + #review - we should probably provide a more narrow filter than only strips color - and one that strips most(?) + # - but does it ever really make sense to strip things like "esc(0" and "esc(B" which flip to the G0 G1 characters? (once stripped - things like box-lines become ordinary letters - unlikely to be desired?) + #punk::ansi::ansistrip converts at least some of the box drawing G0 chars to unicode - todo - more complete conversion + #assumes line-buffering. a more advanced filter required if ansicodes can arrive split across separate read or write operations! + oo::class create ansistrip { + variable o_trecord + variable o_enc + variable o_is_junction + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [dict get $tf -encoding] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + } + method initialize {transform_handle mode} { + return [list initialize read write clear flush drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method clear {transform_handle} { + return + } + method watch {transform_handle events} { + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method flush {transform_handle} { + return "" + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + set outstring [punk::ansi::ansistrip $instring] + return [encoding convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + + #a test + oo::class create reconvert { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + } + oo::define reconvert { + method meta_is_redirection {} { + return 0 + } + } + + + #this isn't a particularly nice thing to do to a stream - especially if someone isn't expecting ansi codes sprinkled through it. + #It can be useful for test/debugging + #Due to chunking at random breaks - we have to check if an ansi code in the underlying stream has been split - otherwise our wrapping will break the existing ansi + # + set sixelstart_re {\x1bP([;0-9]*)q} ;#7-bit - todo 8bit + #todo kitty graphics \x1b_G... + #todo iterm graphics + + oo::class create ansiwrap { + variable o_trecord + variable o_enc + variable o_encbuf ;#buffering for partial encoding bytes + variable o_colour + variable o_do_colour + variable o_do_colourlist + variable o_do_normal + variable o_is_junction + variable o_codestack + variable o_gx_state ;#on/off alt graphics + variable o_buffered ;#buffering for partial ansi codes + constructor {tf} { + package require punk::ansi + set o_trecord $tf + set o_enc [tcl::dict::get $tf -encoding] + set settingsdict [tcl::dict::get $tf -settings] + if {[tcl::dict::exists $settingsdict -colour]} { + set o_colour [tcl::dict::get $settingsdict -colour] + #warning - we can't merge certain extended attributes such as undercurly into single SGR escape sequence + #while some terminals may handle these extended attributes even when merged - we need to cater for those that + #don't. Keeping them as a separate escape allows terminals that don't handle them to ignore just that code without + #affecting the interpretation of the other codes. + set o_do_colour [punk::ansi::a+ {*}$o_colour] + set o_do_colourlist [punk::ansi::ta::get_codes_single $o_do_colour] + set o_do_normal [punk::ansi::a] + } else { + set o_colour {} + set o_do_colour "" + set o_do_colourlist {} + set o_do_normal "" + } + set o_codestack [list] + set o_gx_state [expr {off}] + set o_encbuf "" + set o_buffered "" ;#hold back data that potentially contains partial ansi codes + if {[tcl::dict::exists $tf -junction]} { + set o_is_junction [tcl::dict::get $tf -junction] + } else { + set o_is_junction 0 + } + } + + + #todo - track when in sixel,iterm,kitty graphics data - can be very large + method Trackcodes {chunk} { + #note - caller can use 2 resets in a single unit to temporarily reset to no sgr (override ansiwrap filter) + #e.g [a+ reset reset] (0;0m vs 0;m) + + #puts stdout "===[ansistring VIEW -lf 1 $o_buffered]" + set buf $o_buffered$chunk + set emit "" + if {[string last \x1b $buf] >= 0} { + #detect will detect ansi SGR and gron groff and other codes + if {[punk::ansi::ta::detect $buf]} { + #split_codes_single regex faster than split_codes - but more resulting parts + #'single' refers to number of escapes - but can still contain e.g multiple SGR codes (or mode set operations etc) + set parts [punk::ansi::ta::split_codes_single $buf] + #process all pt/code pairs except for trailing pt + foreach {pt code} [lrange $parts 0 end-1] { + #puts "<==[ansistring VIEW -lf 1 $pt]==>" + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$pt + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # append emit $o_do_colour$pt$o_do_normal + # #append emit $pt + #} else { + # append emit $pt + #} + + set c1c2 [tcl::string::range $code 0 1] + set leadernorm [tcl::string::range [tcl::string::map [list\ + \x1b\[ 7CSI\ + \x9b 8CSI\ + \x1b\( 7GFX\ + ] $c1c2] 0 3] + switch -- $leadernorm { + 7CSI - 8CSI { + if {[punk::ansi::codetype::is_sgr_reset $code]} { + set o_codestack [list "\x1b\[m"] + } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { + set o_codestack [list $code] + } elseif {[punk::ansi::codetype::is_sgr $code]} { + #todo - make caching is_sgr method + set dup_posns [lsearch -all -exact $o_codestack $code] + set o_codestack [lremove $o_codestack {*}$dup_posns] + lappend o_codestack $code + } else { + + } + } + 7GFX { + switch -- [tcl::string::index $code 2] { + "0" { + set o_gx_state on + } + "B" { + set o_gx_state off + } + } + } + default { + #other ansi codes + } + } + append emit $code + } + + + set trailing_pt [lindex $parts end] + if {[string first \x1b $trailing_pt] >= 0} { + #puts stdout "...[ansistring VIEW -lf 1 $trailing_pt]...buffered:<[ansistring VIEW $o_buffered]> '[ansistring VIEW -lf 1 $emit]'" + #may not be plaintext after all + set o_buffered $trailing_pt + #puts stdout "=-=[ansistring VIEWCODES $o_buffered]" + } else { + #puts [a+ yellow]???[ansistring VIEW "'$o_buffered'<+>'$trailing_pt'"]???[a] + switch -- [llength $o_codestack] { + 0 { + append emit $o_do_colour$trailing_pt$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + append emit $o_do_colour$trailing_pt$o_do_normal + set o_codestack [list] + } else { + #append emit [lindex $o_codestack 0]$trailing_pt + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + default { + append emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$trailing_pt + } + } + #if {![llength $o_codestack] || ([llength $o_codestack] ==1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]])} { + # append emit $o_do_colour$trailing_pt$o_do_normal + #} else { + # append emit $trailing_pt + #} + #the previous o_buffered formed the data we emitted - nothing new to buffer because we emitted all parts including the trailing plaintext + set o_buffered "" + } + + + } else { + #REVIEW - this holding a buffer without emitting as we go is ugly. + # - we may do better to detect and retain the opener, then use that opener to avoid false splits within the sequence. + # - we'd then need to detect the appropriate close to restart splitting and codestacking + # - we may still need to retain and append the data to the opener (in some cases?) - which is a slight memory issue - but at least we would emit everything immediately. + + + #puts "-->esc but no detect" + #no complete ansi codes - but at least one esc is present + if {[string index $buf end] eq "\x1b" && [string first \x1b $buf] == [string length $buf]-1} { + #string index in first part of && clause to avoid some unneeded scans of whole string for this test + #we can't use 'string last' - as we need to know only esc is last char in buf + #puts ">>trailing-esc<<" + set o_buffered \x1b + set emit $o_do_colour[string range $buf 0 end-1]$o_do_normal + #set emit [string range $buf 0 end-1] + set buf "" + } else { + set emit_anyway 0 + #todo - ensure non-ansi escapes in middle of chunks don't lead to ever growing buffer + if {[punk::ansi::ta::detect_st_open $buf]} { + #no detect - but we have an ST open (privacy msg etc) - allow a larger chunk before we give up - could include newlines (and even nested codes - although not widely interpreted that way in terms) + set st_partial_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of unclosed ST code + #todo - configurable ST max - use 1k for now + if {$st_partial_len < 1001} { + append o_buffered $chunk + set emit "" + set buf "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } else { + set possible_code_len [expr {[string length $buf] - [string last \x1b $buf]}] ;#length of possible code + #most opening sequences are 1,2 or 3 chars - review? + set open_sequence_detected [punk::ansi::ta::detect_open $buf] + if {$possible_code_len > 10 && !$open_sequence_detected} { + set emit_anyway 1 + set o_buffered "" + } else { + #could be composite sequence with params - allow some reasonable max sequence length + #todo - configurable max sequence length + #len 40-50 quite possible for SGR sequence using coloured underlines etc, even without redundancies + # - allow some headroom for redundant codes when the caller didn't merge. + if {$possible_code_len < 101} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + #allow a little more grace if we at least have an opening ansi sequence of any type.. + if {$open_sequence_detected && $possible_code_len < 151} { + append o_buffered $chunk + set buf "" + set emit "" + } else { + set emit_anyway 1 + set o_buffered "" + } + } + } + } + if {$emit_anyway} { + #assert: any time emit_anyway == 1 buf already contains all of previous o_buffered and o_buffered has been cleared. + + #looked ansi-like - but we've given enough length without detecting close.. + #treat as possible plain text with some esc or unrecognised ansi sequence + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + #if {( ![llength $o_codestack] || ([llength $o_codestack] == 1 && [punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]))} { + # set emit $o_do_colour$buf$o_do_normal + #} else { + # set emit $buf + #} + } + } + } + } else { + #no esc + #puts stdout [a+ yellow]...[a] + #test! + switch -- [llength $o_codestack] { + 0 { + set emit $o_do_colour$buf$o_do_normal + } + 1 { + if {[punk::ansi::codetype::is_sgr_reset [lindex $o_codestack 0]]} { + set emit $o_do_colour$buf$o_do_normal + set o_codestack [list] + } else { + #set emit [lindex $o_codestack 0]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + default { + #set emit [punk::ansi::codetype::sgr_merge_singles $o_codestack]$buf + set emit [punk::ansi::codetype::sgr_merge_singles [list {*}$o_do_colourlist {*}$o_codestack]]$buf + } + } + set o_buffered "" + } + return [dict create emit $emit stacksize [llength $o_codestack]] + } + method initialize {transform_handle mode} { + #clear undesirable in terminal output channels (review) + return [list initialize write flush read drain finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method clear {transform_handle} { + #In the context of stderr/stdout - we probably don't want clear to run. + #Terminals might call it in the middle of a split ansi code - resulting in broken output. + #Leave clear of it the init call + puts stdout "" + set emit [tcl::encoding::convertto $o_enc $o_buffered] + set o_buffered "" + return $emit + } + method flush {transform_handle} { + #puts stdout "" + set inputbytes $o_buffered$o_encbuf + set emit [tcl::encoding::convertto $o_enc $inputbytes] + set o_buffered "" + set o_encbuf "" + return $emit + } + method write {transform_handle bytes} { + #set instring [tcl::encoding::convertfrom $o_enc $bytes] ;naive approach will break due to unexpected byte sequence - occasionally + #bytes can break at arbitrary points making encoding conversions invalid. + + set inputbytes $o_encbuf$bytes + set o_encbuf "" + set tail_offset 0 + while {$tail_offset < [string length $inputbytes] && [catch {tcl::encoding::convertfrom $o_enc [string range $inputbytes 0 end-$tail_offset]} stringdata]} { + incr tail_offset + } + if {$tail_offset > 0} { + if {$tail_offset < [string length $inputbytes]} { + #stringdata from catch statement must be a valid result + set t [expr {$tail_offset - 1}] + set o_encbuf [string range $inputbytes end-$t end] + } else { + set stringdata "" + set o_encbuf $inputbytes + return "" + } + } + set streaminfo [my Trackcodes $stringdata] + set emit [dict get $streaminfo emit] + + #review - wrapping already done in Trackcodes + #if {[dict get $streaminfo stacksize] == 0} { + # #no ansi on the stack - we can wrap + # #review + # set outstring "$o_do_colour$emit$o_do_normal" + #} else { + #} + #if {[llength $o_codestack]} { + # set outstring [punk::ansi::codetype::sgr_merge_singles $o_codestack]$emit + #} else { + # set outstring $emit + #} + #set outstring $emit + + #puts stdout "decoded >>>[ansistring VIEWCODES $outstring]<<<" + #puts stdout "re-encoded>>>[ansistring VIEW [tcl::encoding::convertto $o_enc $outstring]]<<<" + return [tcl::encoding::convertto $o_enc $emit] + } + method Write_naive {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + #set outstring ">>>$instring" + return [tcl::encoding::convertto $o_enc $outstring] + } + method drain {transform_handle} { + return "" + } + method read {transform_handle bytes} { + set instring [tcl::encoding::convertfrom $o_enc $bytes] + set outstring "$o_do_colour$instring$o_do_normal" + return [tcl::encoding::convertto $o_enc $outstring] + } + method meta_is_redirection {} { + return $o_is_junction + } + } + #todo - something + oo::class create rebuffer { + variable o_trecord + variable o_enc + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + } + method initialize {transform_handle mode} { + return [list initialize read write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + #set outstring [string map [list \n ] $instring] + set outstring $instring + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define rebuffer { + method meta_is_redirection {} { + return 0 + } + } + + #has slight buffering/withholding of lone training cr - we can't be sure that a cr at end of chunk is part of \r\n sequence + oo::class create tounix { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \n} $instring] + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define tounix { + method meta_is_redirection {} { + return $o_is_junction + } + } + #write to handle case where line-endings already \r\n too + oo::class create towindows { + variable o_trecord + variable o_enc + variable o_last_char_was_cr + variable o_is_junction + constructor {tf} { + set o_trecord $tf + set o_enc [dict get $tf -encoding] + set settingsdict [dict get $tf -settings] + if {[dict exists $tf -junction]} { + set o_is_junction [dict get $tf -junction] + } else { + set o_is_junction 0 + } + set o_last_char_was_cr 0 + } + method initialize {transform_handle mode} { + return [list initialize write finalize] + } + method finalize {transform_handle} { + my destroy + } + method watch {transform_handle events} { + } + #don't use read + method read {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + + set outstring $instring + + return [encoding convertto $o_enc $outstring] + } + method write {transform_handle bytes} { + set instring [encoding convertfrom $o_enc $bytes] + #set outstring [string map [list \n ] $instring] + + if {$o_last_char_was_cr} { + set instring "\r$instring" + } + + set outstring [string map {\r\n \uFFFF} $instring] + set outstring [string map {\n \r\n} $outstring] + set outstring [string map {\uFFFF \r\n} $outstring] + + set lastchar [string range $outstring end end] + if {$lastchar eq "\r"} { + set o_last_char_was_cr 1 + set outstring [string range $outstring 0 end-1] + } else { + set o_last_char_was_cr 0 + } + #review! can we detect eof here on the transform_handle? + #if eof, we don't want to strip a trailing \r + + return [encoding convertto $o_enc $outstring] + #return [encoding convertto utf-16le $outstring] + } + } + oo::define towindows { + method meta_is_redirection {} { + return $o_is_junction + } + } + + } +} + +# ---------------------------------------------------------------------------- +#review float/sink metaphor. +#perhaps something with the concept of upstream and downstream? +#need concepts for push towards data, sit in middle where placed, and lag at tail of data stream. +## upstream for stdin is at the bottom of the stack and for stdout is the top of the stack. +#upstream,neutral-upstream,downstream,downstream-aside,downstream-replace (default neutral-upstream - require action 'stack' to use standard channel stacking concept and ignore other actions) +#This is is a bit different from the float/sink metaphor which refers to the channel stacking order as opposed to the data-flow direction. +#The idea would be that whether input or output +# upstream additions go to the side closest to the datasource +# downstream additions go furthest from the datasource +# - all new additions go ahead of any diversions as the most upstream diversion is the current end of the stream in a way. +# - this needs review regarding subsequent removal of the diversion and whether filters re-order in response.. +# or if downstream & neutral additions are reclassified upon insertion if they land among existing upstreams(?) +# neutral-upstream goes to the datasource side of the neutral-upstream list. +# No 'neutral' option provided so that we avoid the need to think forwards or backwards when adding stdin vs stdout shellfilter does the necessary pop/push reordering. +# No 'neutral-downstream' to reduce complexity. +# downstream-replace & downstream-aside head downstream to the first diversion they encounter. ie these actions are no longer referring to the stack direction but only the dataflow direction. +# +# ---------------------------------------------------------------------------- +# +# 'filters' are transforms that don't redirect +# - limited range of actions to reduce complexity. +# - any requirement not fulfilled by float,sink,sink-replace,sink-sideline should be done by multiple pops and pushes +# +#actions can float to top of filters or sink to bottom of filters +#when action is of type sink, it can optionally replace or sideline the first non-filter it encounters (highest redirection on the stack.. any lower are starved of the stream anyway) +# - sideline means to temporarily replace the item and keep a record, restoring if/when we are removed from the transform stack +# +##when action is of type float it can't replace or sideline anything. A float is added above any existing floats and they stay in the same order relative to each other, +#but non-floats added later will sit below all floats. +#(review - float/sink initially designed around output channels. For stdin the dataflow is reversed. implement float-aside etc?) +# +# +#action: float sink sink-replace,sink-sideline +# +# +## note - whether stack is for input or output we maintain it in the same direction - which is in sync with the tcl chan pop chan push concept. +## +namespace eval shellfilter::stack { + namespace export {[a-z]*} + namespace ensemble create + #todo - implement as oo ? + variable pipelines [list] + + proc items {} { + #review - stdin,stdout,stderr act as pre-existing pipelines, and we can't create a new one with these names - so they should probably be autoconfigured and listed.. + # - but in what contexts? only when we find them in [chan names]? + variable pipelines + return [dict keys $pipelines] + } + proc item {pipename} { + variable pipelines + return [dict get $pipelines $pipename] + } + proc item_tophandle {pipename} { + variable pipelines + set handle "" + if {[dict exists $pipelines $pipename stack]} { + set stack [dict get $pipelines $pipename stack] + set topstack [lindex $stack end] ;#last item in stack is top (for output channels anyway) review comment. input chans? + if {$topstack ne ""} { + if {[dict exists $topstack -handle]} { + set handle [dict get $topstack -handle] + } + } + } + return $handle + } + proc status {{pipename *} args} { + variable pipelines + set pipecount [dict size $pipelines] + set tabletitle "$pipecount pipelines active" + set t [textblock::class::table new $tabletitle] + $t add_column -headers [list channel-ident] + $t add_column -headers [list device-info localchan] + $t configure_column 1 -header_colspans {3} + $t add_column -headers [list "" remotechan] + $t add_column -headers [list "" tid] + $t add_column -headers [list stack-info] + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + set rc [dict get $pipelines $k device remotechan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "-" + } + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set stackinfo "" + } else { + set tbl_inner [textblock::class::table new] + $tbl_inner configure -show_edge 0 + foreach rec $stack { + set handle [punk::lib::dict_getdef $rec -handle ""] + set id [punk::lib::dict_getdef $rec -id ""] + set transform [namespace tail [punk::lib::dict_getdef $rec -transform ""]] + set settings [punk::lib::dict_getdef $rec -settings ""] + $tbl_inner add_row [list $id $transform $handle $settings] + } + set stackinfo [$tbl_inner print] + $tbl_inner destroy + } + $t add_row [list $k $lc $rc $tid $stackinfo] + } + set result [$t print] + $t destroy + return $result + } + proc status1 {{pipename *} args} { + variable pipelines + + set pipecount [dict size $pipelines] + set tableprefix "$pipecount pipelines active\n" + foreach p [dict keys $pipelines] { + append tableprefix " " $p \n + } + package require overtype + #todo -verbose + set table "" + set ac1 [string repeat " " 15] + set ac2 [string repeat " " 42] + set ac3 [string repeat " " 70] + append table "[overtype::left $ac1 channel-ident] " + append table "[overtype::left $ac2 device-info] " + append table "[overtype::left $ac3 stack-info]" + append table \n + + + set bc1 [string repeat " " 5] ;#stack id + set bc2 [string repeat " " 25] ;#transform + set bc3 [string repeat " " 50] ;#settings + + foreach k [dict keys $pipelines $pipename] { + set lc [dict get $pipelines $k device localchan] + if {[dict exists $k device workertid]} { + set tid [dict get $pipelines $k device workertid] + } else { + set tid "" + } + + + set col1 [overtype::left $ac1 $k] + set col2 [overtype::left $ac2 "localchan: $lc tid:$tid"] + + set stack [dict get $pipelines $k stack] + if {![llength $stack]} { + set col3 $ac3 + } else { + set rec [lindex $stack 0] + set bcol1 [overtype::left $bc1 [dict get $rec -id]] + set bcol2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bcol3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bcol1 $bcol2 $bcol3" + set col3 [overtype::left $ac3 $stackrow] + } + + append table "$col1 $col2 $col3\n" + + + foreach rec [lrange $stack 1 end] { + set col1 $ac1 + set col2 $ac2 + if {[llength $rec]} { + set bc1 [overtype::left $bc1 [dict get $rec -id]] + set bc2 [overtype::left $bc2 [namespace tail [dict get $rec -transform]]] + set bc3 [overtype::left $bc3 [dict get $rec -settings]] + set stackrow "$bc1 $bc2 $bc3" + set col3 [overtype::left $ac3 $stackrow] + } else { + set col3 $ac3 + } + append table "$col1 $col2 $col3\n" + } + + } + return $tableprefix$table + } + #used for output channels - we usually want to sink redirections below the floaters and down to topmost existing redir + proc _get_stack_floaters {stack} { + set floaters [list] + foreach t [lreverse $stack] { + switch -- [dict get $t -action] { + float { + lappend floaters $t + } + default { + break + } + } + } + return [lreverse $floaters] + } + + + + #for output-channel sinking + proc _get_stack_top_redirection {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + incr r + } + #not found + return [list index -1 record {}] + } + #exclude float-locked, locked, sink-locked + proc _get_stack_top_redirection_replaceable {stack} { + set r 0 ;#reverse index + foreach t [lreverse $stack] { + set action [dict get $t -action] + if {![string match "*locked*" $action]} { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + set idx [expr {[llength $stack] - ($r + 1) }] ;#forward index + return [list index $idx record $t] + } + } + incr r + } + #not found + return [list index -1 record {}] + } + + + #for input-channels ? + proc _get_stack_bottom_redirection {stack} { + set i 0 + foreach t $stack { + set obj [dict get $t -obj] + if {[$obj meta_is_redirection]} { + return [linst index $i record $t] + } + incr i + } + #not found + return [list index -1 record {}] + } + + + proc get_next_counter {pipename} { + variable pipelines + #use dictn incr ? + set counter [dict get $pipelines $pipename counter] + incr counter + dict set pipelines $pipename counter $counter + return $counter + } + + proc unwind {pipename} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + foreach tf [lreverse $stack] { + chan pop $localchan + } + dict set pipelines $pipename [list] + } + #todo + proc delete {pipename {wait 0}} { + variable pipelines + set pipeinfo [dict get $pipelines $pipename] + set deviceinfo [dict get $pipeinfo device] + set localchan [dict get $deviceinfo localchan] + unwind $pipename + + #release associated thread + set tid [dict get $deviceinfo workertid] + if {$wait} { + thread::release -wait $tid + } else { + thread::release $tid + } + + #Memchan closes without error - tcl::chan::fifo2 raises something like 'can not find channel named "rc977"' - REVIEW. why? + catch {chan close $localchan} + } + #review - proc name clarity is questionable. remove_stackitem? + proc remove {pipename remove_id} { + variable pipelines + if {![dict exists $pipelines $pipename]} { + puts stderr "WARNING: shellfilter::stack::remove pipename '$pipename' not found in pipelines dict: '$pipelines' [info level -1]" + return + } + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $pipelines $pipename device localchan] + set posn 0 + set idposn -1 + set asideposn -1 + foreach t $stack { + set id [dict get $t -id] + if {$id eq $remove_id} { + set idposn $posn + break + } + #look into asides (only can be one for now) + if {[llength [dict get $t -aside]]} { + set a [dict get $t -aside] + if {[dict get $a -id] eq $remove_id} { + set asideposn $posn + break + } + } + incr posn + } + + if {$asideposn > 0} { + #id wasn't found directly in stack, but in an -aside. we don't need to pop anything - just clear this aside record + set container [lindex $stack $asideposn] + dict set container -aside {} + lset stack $asideposn $container + dict set pipelines $pipename stack $stack + } else { + if {$idposn < 0} { + ::shellfilter::log::write shellfilter "ERROR shellfilter::stack::remove $pipename id '$remove_id' not found" + puts stderr "|WARNING>shellfilter::stack::remove $pipename id '$remove_id' not found" + return 0 + } + set removed_item [lindex $stack $idposn] + + #include idposn in poplist + set poplist [lrange $stack $idposn end] + set stack [lreplace $stack $idposn end] + #pop all chans before adding anything back in! + foreach p $poplist { + chan pop $localchan + } + + if {[llength [dict get $removed_item -aside]]} { + set restore [dict get $removed_item -aside] + set t [dict get $restore -transform] + set tsettings [dict get $restore -settings] + set obj [$t new $restore] + set h [chan push $localchan $obj] + dict set restore -handle $h + dict set restore -obj $obj + lappend stack $restore + } + + #put popped back except for the first one, which we want to remove + foreach p [lrange $poplist 1 end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + dict set p -handle $h + dict set p -obj $obj + lappend stack $p + } + dict set pipelines $pipename stack $stack + } + #JMNJMN 2025 review! + #show_pipeline $pipename -note "after_remove $remove_id" + return 1 + } + + #pop a number of items of the top of the stack, add our transform record, and add back all (or the tail of poplist if pushstartindex > 0) + proc insert_transform {pipename stack transformrecord poplist {pushstartindex 0}} { + variable pipelines + set bottom_pop_posn [expr {[llength $stack] - [llength $poplist]}] + set poplist [lrange $stack $bottom_pop_posn end] + set stack [lreplace $stack $bottom_pop_posn end] + + set localchan [dict get $pipelines $pipename device localchan] + foreach p [lreverse $poplist] { + chan pop $localchan + } + set transformname [dict get $transformrecord -transform] + set transformsettings [dict get $transformrecord -settings] + set obj [$transformname new $transformrecord] + set h [chan push $localchan $obj] + dict set transformrecord -handle $h + dict set transformrecord -obj $obj + dict set transformrecord -note "insert_transform" + lappend stack $transformrecord + foreach p [lrange $poplist $pushstartindex end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added" + + lappend stack $p + } + return $stack + } + + #fifo2 + proc new {pipename args} { + variable pipelines + if {($pipename in [dict keys $pipelines]) || ($pipename in [chan names])} { + error "shellfilter::stack::new error: pipename '$pipename' already exists" + } + + set opts [dict merge {-settings {}} $args] + set defaultsettings [dict create -raw 1 -buffering line -direction out] + set targetsettings [dict merge $defaultsettings [dict get $opts -settings]] + + set direction [dict get $targetsettings -direction] + + #pipename is the source/facility-name ? + if {$direction eq "out"} { + set pipeinfo [shellfilter::pipe::open_out $pipename $targetsettings] + } else { + puts stderr "|jn> pipe::open_in $pipename $targetsettings" + set pipeinfo [shellfilter::pipe::open_in $pipename $targetsettings] + } + #open_out/open_in will configure buffering based on targetsettings + + set program_chan [dict get $pipeinfo localchan] + set worker_chan [dict get $pipeinfo remotechan] + set workertid [dict get $pipeinfo workertid] + + + set deviceinfo [dict create pipename $pipename localchan $program_chan remotechan $worker_chan workertid $workertid direction $direction] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + + return $deviceinfo + } + #we 'add' rather than 'push' because transforms can float,sink and replace/sideline so they don't necessarily go to the top of the transform stack + proc add {pipename transformname args} { + variable pipelines + #chan names doesn't reflect available channels when transforms are in place + #e.g stdout may exist but show as something like file191f5b0dd80 + if {($pipename ni [dict keys $pipelines])} { + if {[catch {eof $pipename} is_eof]} { + error "shellfilter::stack::add no existing chan or pipename matching '$pipename' in channels:[chan names] or pipelines:$pipelines use stdin/stderr/stdout or shellfilter::stack::new " + } + } + set args [dict merge {-action "" -settings {}} $args] + set action [dict get $args -action] + set transformsettings [dict get $args -settings] + if {[string first "::" $transformname] < 0} { + set transformname ::shellfilter::chan::$transformname + } + if {![llength [info commands $transformname]]} { + error "shellfilter::stack::push unknown transform '$transformname'" + } + + + if {![dict exists $pipelines $pipename]} { + #pipename must be in chan names - existing device/chan + #record a -read and -write end even if the device is only being used as one or the other + set deviceinfo [dict create pipename $pipename localchan $pipename remotechan {}] + dict set pipelines $pipename [list counter 0 device $deviceinfo stack [list]] + } else { + set deviceinfo [dict get $pipelines $pipename device] + } + + set id [get_next_counter $pipename] + set stack [dict get $pipelines $pipename stack] + set localchan [dict get $deviceinfo localchan] + + #we redundantly store chan in each transform - makes debugging clearer + # -encoding similarly could be stored only at the pipeline level (or even queried directly each filter-read/write), + # but here it may help detect unexpected changes during lifetime of the stack and avoids the chance of callers incorrectly using the transform handle?) + # jn + set transform_record [list -id $id -chan $pipename -encoding [chan configure $localchan -encoding] -transform $transformname -aside {} {*}$args] + switch -glob -- $action { + float - float-locked { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } + "" - locked { + set floaters [_get_stack_floaters $stack] + if {![llength $floaters]} { + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + lappend stack $transform_record + } else { + set poplist $floaters + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + "sink*" { + set redirinfo [_get_stack_top_redirection $stack] + set idx_existing_redir [dict get $redirinfo index] + if {$idx_existing_redir == -1} { + #no existing redirection transform on the stack + #pop everything.. add this record as the first redirection on the stack + set poplist $stack + set stack [insert_transform $pipename $stack $transform_record $poplist] + } else { + switch -glob -- $action { + "sink-replace" { + #include that index in the poplist + set poplist [lrange $stack $idx_existing_redir end] + #pop all from idx_existing_redir to end, but put back 'lrange $poplist 1 end' + set stack [insert_transform $pipename $stack $transform_record $poplist 1] + } + "sink-aside*" { + set existing_redir_record [lindex $stack $idx_existing_redir] + if {[string match "*locked*" [dict get $existing_redir_record -action]]} { + set put_aside 0 + #we can't aside this one - sit above it instead. + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [lrange $stack 0 $idx_existing_redir] + } else { + set put_aside 1 + dict set transform_record -aside [lindex $stack $idx_existing_redir] + set poplist [lrange $stack $idx_existing_redir end] + set stack [lrange $stack 0 $idx_existing_redir-1] + } + foreach p $poplist { + chan pop $localchan + } + set transformname [dict get $transform_record -transform] + set transform_settings [dict get $transform_record -settings] + set obj [$transformname new $transform_record] + set h [chan push $localchan $obj] + dict set transform_record -handle $h + dict set transform_record -obj $obj + dict set transform_record -note "insert_transform-with-aside" + lappend stack $transform_record + #add back poplist *except* the one we transferred into -aside (if we were able) + foreach p [lrange $poplist $put_aside end] { + set t [dict get $p -transform] + set tsettings [dict get $p -settings] + set obj [$t new $p] + set h [chan push $localchan $obj] + #retain previous -id - code that added it may have kept reference and not expecting it to change + dict set p -handle $h + dict set p -obj $obj + dict set p -note "re-added-after-sink-aside" + lappend stack $p + } + } + default { + #plain "sink" + #we only sink to the topmost redirecting filter - which makes sense for an output channel + #For stdin.. this is more problematic as we're more likely to want to intercept the bottom most redirection. + #todo - review. Consider making default insert position for input channels to be at the source... and float/sink from there. + # - we don't currently know from the stack api if adding input vs output channel - so this needs work to make intuitive. + # consider splitting stack::add to stack::addinput stack::addoutput to split the different behaviour + set poplist [lrange $stack $idx_existing_redir+1 end] + set stack [insert_transform $pipename $stack $transform_record $poplist] + } + } + } + } + default { + error "shellfilter::stack::add unimplemented action '$action'" + } + } + + dict set pipelines $pipename stack $stack + #puts stdout "==" + #puts stdout "==>stack: $stack" + #puts stdout "==" + + #JMNJMN + #show_pipeline $pipename -note "after_add $transformname $args" + return $id + } + proc show_pipeline {pipename args} { + variable pipelines + set stack [dict get $pipelines $pipename stack] + set tag "SHELLFILTER::STACK" + #JMN - load from config + #::shellfilter::log::open $tag {-syslog 127.0.0.1:514} + if {[catch { + ::shellfilter::log::open $tag {-syslog ""} + } err]} { + #e.g safebase interp can't load required modules such as shellthread (or Thread) + puts stderr "shellfilter::show_pipeline cannot open log" + return + } + ::shellfilter::log::write $tag "transform stack for $pipename $args" + foreach tf $stack { + ::shellfilter::log::write $tag " $tf" + } + + } +} + + +namespace eval shellfilter { + variable sources [list] + variable stacks [dict create] + + proc ::shellfilter::redir_channel_to_log {chan args} { + variable sources + set default_logsettings [dict create \ + -tag redirected_$chan -syslog "" -file ""\ + ] + if {[dict exists $args -action]} { + set action [dict get $args -action] + } else { + # action "sink" is a somewhat reasonable default for an output redirection transform + # but it can make it harder to configure a plain ordered stack if the user is not expecting it, so we'll default to stack + # also.. for stdin transform sink makes less sense.. + #todo - default "stack" instead of empty string + set action "" + } + if {[dict exists $args -settings]} { + set logsettings [dict get $args -settings] + } else { + set logsettings {} + } + + set logsettings [dict merge $default_logsettings $logsettings] + set tag [dict get $logsettings -tag] + if {$tag ni $sources} { + lappend sources $tag + } + + set id [shellfilter::stack::add $chan logonly -action $action -settings $logsettings] + return $id + } + + proc ::shellfilter::redir_output_to_log {tagprefix args} { + variable sources + + set default_settings [list -tag ${tagprefix} -syslog "" -file ""] + + set opts [dict create -action "" -settings {}] + set opts [dict merge $opts $args] + set optsettings [dict get $opts -settings] + set settings [dict merge $default_settings $optsettings] + + set tag [dict get $settings -tag] + if {$tag ne $tagprefix} { + error "shellfilter::redir_output_to_log -tag value must match supplied tagprefix:'$tagprefix'. Omit -tag, or make it the same. It will automatically be suffixed with stderr and stdout. Use redir_channel_to_log if you want to separately configure each channel" + } + lappend sources ${tagprefix}stdout ${tagprefix}stderr + + set stdoutsettings $settings + dict set stdoutsettings -tag ${tagprefix}stdout + set stderrsettings $settings + dict set stderrsettings -tag ${tagprefix}stderr + + set idout [redir_channel_to_log stdout -action [dict get $opts -action] -settings $stdoutsettings] + set iderr [redir_channel_to_log stderr -action [dict get $opts -action] -settings $stderrsettings] + + return [list $idout $iderr] + } + + #eg try: set v [list #a b c] + #vs set v {#a b c} + proc list_is_canonical l { + #courtesy DKF via wiki https://wiki.tcl-lang.org/page/BNF+for+Tcl + if {[catch {llength $l}]} {return 0} + string equal $l [list {*}$l] + } + + #return a dict keyed on numerical list index showing info about each element + # - particularly + # 'wouldbrace' to indicate that the item would get braced by Tcl when added to another list + # 'head_tail_chars' to show current first and last character (in case it's wrapped e.g in double or single quotes or an existing set of braces) + proc list_element_info {inputlist} { + set i 0 + set info [dict create] + set testlist [list] + foreach original_item $inputlist { + #--- + # avoid sharing internal rep with original items in the list (avoids shimmering of rep in original list for certain items such as paths) + unset -nocomplain item + append item $original_item {} + #--- + + set iteminfo [dict create] + set itemlen [string length $item] + lappend testlist $item + set tcl_len [string length $testlist] + set diff [expr {$tcl_len - $itemlen}] + if {$diff == 0} { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 0 + } else { + #test for escaping vs bracing! + set testlistchars [split $testlist ""] + if {([lindex $testlistchars 0] eq "\{") && ([lindex $testlistchars end] eq "\}")} { + dict set iteminfo wouldbrace 1 + dict set iteminfo wouldescape 0 + } else { + dict set iteminfo wouldbrace 0 + dict set iteminfo wouldescape 1 + } + } + set testlist [list] + set charlist [split $item ""] + set char_a [lindex $charlist 0] + set char_b [lindex $charlist 1] + set char_ab ${char_a}${char_b} + set char_y [lindex $charlist end-1] + set char_z [lindex $charlist end] + set char_yz ${char_y}${char_z} + + if { ("{" in $charlist) || ("}" in $charlist) } { + dict set iteminfo has_braces 1 + set innerchars [lrange $charlist 1 end-1] + if {("{" in $innerchars) || ("}" in $innerchars)} { + dict set iteminfo has_inner_braces 1 + } else { + dict set iteminfo has_inner_braces 0 + } + } else { + dict set iteminfo has_braces 0 + dict set iteminfo has_inner_braces 0 + } + + #todo - brace/char counting to determine if actually 'wrapped' + #e.g we could have list element {((abc)} - which appears wrapped if only looking at first and last chars. + #also {(x) (y)} as a list member.. how to treat? + if {$itemlen <= 1} { + dict set iteminfo apparentwrap "not" + } else { + #todo - switch on $char_a$char_z + if {($char_a eq {"}) && ($char_z eq {"})} { + dict set iteminfo apparentwrap "doublequotes" + } elseif {($char_a eq "'") && ($char_z eq "'")} { + dict set iteminfo apparentwrap "singlequotes" + } elseif {($char_a eq "(") && ($char_z eq ")")} { + dict set iteminfo apparentwrap "brackets" + } elseif {($char_a eq "\{") && ($char_z eq "\}")} { + dict set iteminfo apparentwrap "braces" + } elseif {($char_a eq "^") && ($char_z eq "^")} { + dict set iteminfo apparentwrap "carets" + } elseif {($char_a eq "\[") && ($char_z eq "\]")} { + dict set iteminfo apparentwrap "squarebrackets" + } elseif {($char_a eq "`") && ($char_z eq "`")} { + dict set iteminfo apparentwrap "backquotes" + } elseif {($char_a eq "\n") && ($char_z eq "\n")} { + dict set iteminfo apparentwrap "lf-newline" + } elseif {($char_ab eq "\r\n") && ($char_yz eq "\r\n")} { + dict set iteminfo apparentwrap "crlf-newline" + } else { + dict set iteminfo apparentwrap "not-determined" + } + + } + dict set iteminfo wrapbalance "unknown" ;#a hint to caller that apparentwrap is only a guide. todo - possibly make wrapbalance indicate 0 for unbalanced.. and positive numbers for outer-count of wrappings. + #e.g {((x)} == 0 {((x))} == 1 {(x) (y (z))} == 2 + dict set iteminfo head_tail_chars [list $char_a $char_z] + set namemap [list \ + \r cr\ + \n lf\ + {"} doublequote\ + {'} singlequote\ + "`" backquote\ + "^" caret\ + \t tab\ + " " sp\ + "\[" lsquare\ + "\]" rsquare\ + "(" lbracket\ + ")" rbracket\ + "\{" lbrace\ + "\}" rbrace\ + \\ backslash\ + / forwardslash\ + ] + if {[string length $char_a]} { + set char_a_name [string map $namemap $char_a] + } else { + set char_a_name "emptystring" + } + if {[string length $char_z]} { + set char_z_name [string map $namemap $char_z] + } else { + set char_z_name "emptystring" + } + + dict set iteminfo head_tail_names [list $char_a_name $char_z_name] + dict set iteminfo len $itemlen + dict set iteminfo difflen $diff ;#2 for braces, 1 for quoting?, or 0. + dict set info $i $iteminfo + incr i + } + return $info + } + + + #parse bracketed expression (e.g produced by vim "shellxquote=(" ) into a tcl (nested) list + #e.g {(^c:/my spacey/path^ >^somewhere^)} + #e.g {(blah (etc))}" + #Result is always a list - even if only one toplevel set of brackets - so it may need [lindex $result 0] if input is the usual case of {( ...)} + # - because it also supports the perhaps less likely case of: {( ...) unbraced (...)} etc + # Note that + #maintenance warning - duplication in branches for bracketed vs unbracketed! + proc parse_cmd_brackets {str} { + #wordwrappers currently best suited to non-bracket entities - no bracket matching within - anything goes until end-token reached. + # - but.. they only take effect where a word can begin. so a[x y] may be split at the space unless it's within some other wraper e.g " a[x y]" will not break at the space + # todo - consider extending the in-word handling of word_bdepth which is currently only applied to () i.e aaa(x y) is supported but aaa[x y] is not as the space breaks the word up. + set wordwrappers [list \ + "\"" [list "\"" "\"" "\""]\ + {^} [list "\"" "\"" "^"]\ + "'" [list "'" "'" "'"]\ + "\{" [list "\{" "\}" "\}"]\ + {[} [list {[} {]} {]}]\ + ] ;#dict mapping start_character to {replacehead replacetail expectedtail} + set shell_specials [list "|" "|&" "<" "<@" "<<" ">" "2>" ">&" ">>" "2>>" ">>&" ">@" "2>@" "2>@1" ">&@" "&" "&&" ] ;#words/chars that may precede an opening bracket but don't merge with the bracket to form a word. + #puts "pb:$str" + set in_bracket 0 + set in_word 0 + set word "" + set result {} + set word_bdepth 0 + set word_bstack [list] + set wordwrap "" ;#only one active at a time + set bracketed_elements [dict create] + foreach char [split $str ""] { + #puts "c:$char bracketed:$bracketed_elements" + if {$in_bracket > 0} { + if {$in_word} { + if {[string length $wordwrap]} { + #anything goes until end-char + #todo - lookahead and only treat as closing if before a space or ")" ? + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + if {$word_bdepth == 0} { + #can potentially close off a word - or start a new one if word-so-far is a shell-special + if {$word in $shell_specials} { + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + } else { + + if {$char eq ")"} { + dict lappend bracketed_elements $in_bracket $word + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + set word "" + set in_word 0 + } elseif {[regexp {[\s]} $char]} { + dict lappend bracketed_elements $in_bracket $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + #ordinary word up-against and opening bracket - brackets are part of word. + incr word_bdepth + append word "(" + } else { + append word $char + } + } + } else { + #currently only () are used for word_bdepth - todo add all or some wordwrappers chars so that the word_bstack can have multiple active. + switch -- $char { + "(" { + incr word_bdepth + lappend word_bstack $char + append word $char + } + ")" { + incr word_bdepth -1 + set word_bstack [lrange $word_bstack 0 end-1] + append word $char + } + default { + #spaces and chars added to word as it's still in a bracketed section + append word $char + } + } + } + } + } else { + + if {$char eq "("} { + incr in_bracket + + } elseif {$char eq ")"} { + set subresult [dict get $bracketed_elements $in_bracket] + dict set bracketed_elements $in_bracket [list] + incr in_bracket -1 + if {$in_bracket == 0} { + lappend result $subresult + } else { + dict lappend bracketed_elements $in_bracket $subresult + } + } elseif {[regexp {[\s]} $char]} { + # + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } else { + if {$in_word} { + if {[string length $wordwrap]} { + lassign [dict get $wordwrappers $wordwrap] _open closing endmark + if {$char eq $endmark} { + set wordwrap "" + append word $closing + lappend result $word + set word "" + set in_word 0 + } else { + append word $char + } + } else { + + if {$word_bdepth == 0} { + if {$word in $shell_specials} { + if {[regexp {[\s]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + lappend result $word + set word "" + set in_word 0 + incr in_bracket + } else { + #at end of shell-specials is another point to look for word started by a wordwrapper char + #- expect common case of things like >^/my/path^ + if {$char in [dict keys $wordwrappers]} { + lappend result $word + set word "" + set in_word 1 ;#just for explicitness.. we're straight into the next word. + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + #something unusual.. keep going with word! + append word $char + } + } + + } else { + if {[regexp {[\s)]} $char]} { + lappend result $word + set word "" + set in_word 0 + } elseif {$char eq "("} { + incr word_bdepth + append word $char + } else { + append word $char + } + } + } else { + switch -- $char { + "(" { + incr word_bdepth + append word $char + } + ")" { + incr word_bdepth -1 + append word $char + } + default { + append word $char + } + } + } + } + } else { + if {[regexp {[\s]} $char]} { + #insig whitespace(?) + } elseif {$char eq "("} { + incr in_bracket + dict set bracketed_elements $in_bracket [list] + } elseif {$char eq ")"} { + error "unbalanced bracket - unable to proceed result so far: $result bracketed_elements:$bracketed_elements" + } else { + #first char of word - look for word-wrappers + if {$char in [dict keys $wordwrappers]} { + set wordwrap $char + set word [lindex [dict get $wordwrappers $char] 0] ;#replace trigger char with the start value it maps to. + } else { + set word $char + } + set in_word 1 + } + } + } + #puts "----$bracketed_elements" + } + if {$in_bracket > 0} { + error "shellfilter::parse_cmd_brackets missing close bracket. input was '$str'" + } + if {[dict exists $bracketed_elements 0]} { + #lappend result [lindex [dict get $bracketed_elements 0] 0] + lappend result [dict get $bracketed_elements 0] + } + if {$in_word} { + lappend result $word + } + return $result + } + + #only double quote if argument not quoted with single or double quotes + proc dquote_if_not_quoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} - {''} { + return $a + } + default { + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + + #proc dquote_if_not_bracketed/braced? + + #wrap in double quotes if not double-quoted + proc dquote_if_not_dquoted {a} { + set wrapchars [string cat [string range $a 0 0] [string range $a end end]] + switch -- $wrapchars { + {""} { + return $a + } + default { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + } + } + proc dquote {a} { + #escape any inner quotes.. + set newinner [string map [list {"} "\\\""] $a] + return "\"$newinner\"" + } + proc get_scriptrun_from_cmdlist_dquote_if_not {cmdlist {shellcmdflag ""}} { + set scr [auto_execok "script"] + if {[string length $scr]} { + #set scriptrun "( $c1 [lrange $cmdlist 1 end] )" + set arg1 [lindex $cmdlist 0] + if {[string first " " $arg1]>0} { + set c1 [dquote_if_not_quoted $arg1] + #set c1 "\"$arg1\"" + } else { + set c1 $arg1 + } + + if {[string length $shellcmdflag]} { + set scriptrun "$shellcmdflag \$($c1 " + } else { + set scriptrun "\$($c1 " + } + #set scriptrun "$c1 " + foreach a [lrange $cmdlist 1 end] { + #set a [string map [list "/" "//"] $a] + #set a [string map [list "\"" "\\\""] $a] + if {[string first " " $a] > 0} { + append scriptrun [dquote_if_not_quoted $a] + } else { + append scriptrun $a + } + append scriptrun " " + } + set scriptrun [string trim $scriptrun] + append scriptrun ")" + #return [list $scr -q -e -c $scriptrun /dev/null] + return [list $scr -e -c $scriptrun /dev/null] + } else { + return $cmdlist + } + } + + proc ::shellfilter::trun {commandlist args} { + #jmn + } + + + # run a command (or tcl script) with tees applied to stdout/stderr/stdin (or whatever channels are being used) + # By the point run is called - any transforms should already be in place on the channels if they're needed. + # The tees will be inline with none,some or all of those transforms depending on how the stack was configured + # (upstream,downstream configured via -float,-sink etc) + proc ::shellfilter::run {commandlist args} { + #must be a list. If it was a shell commandline string. convert it elsewhere first. + + variable sources + set runtag "shellfilter-run" + #set tid [::shellfilter::log::open $runtag [list -syslog 127.0.0.1:514]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + if {[catch {llength $commandlist} listlen]} { + set listlen "" + } + ::shellfilter::log::write $runtag " commandlist:'$commandlist' listlen:$listlen strlen:[string length $commandlist]" + + #flush stdout + #flush stderr + + #adding filters with sink-aside will temporarily disable the existing redirection + #All stderr/stdout from the shellcommand will now tee to the underlying stderr/stdout as well as the configured syslog + + set defaults [dict create \ + -teehandle command \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -tclscript 0 \ + ] + set opts [dict merge $defaults $args] + + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set outchan [dict get $opts -outchan] + set errchan [dict get $opts -errchan] + set inchan [dict get $opts -inchan] + set teehandle [dict get $opts -teehandle] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set is_script [dict get $opts -tclscript] + dict unset opts -tclscript ;#don't pass it any further + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + set teehandle_out ${teehandle}out ;#default commandout + set teehandle_err ${teehandle}err + set teehandle_in ${teehandle}in + + + #puts stdout "shellfilter initialising tee_to_pipe transforms for in/out/err" + + # sources should be added when stack::new called instead(?) + foreach source [list $teehandle_out $teehandle_err] { + if {$source ni $sources} { + lappend sources $source + } + } + set outdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_out device] + set outpipechan [dict get $outdeviceinfo localchan] + set errdeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_err device] + set errpipechan [dict get $errdeviceinfo localchan] + + #set indeviceinfo [dict get $::shellfilter::stack::pipelines $teehandle_in device] + #set inpipechan [dict get $indeviceinfo localchan] + + #NOTE:These transforms are not necessarily at the top of each stack! + #The float/sink mechanism, along with whether existing transforms are diversionary decides where they sit. + set id_out [shellfilter::stack::add $outchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_out -pipechan $outpipechan]] + set id_err [shellfilter::stack::add $errchan tee_to_pipe -action sink-aside -settings [list -tag $teehandle_err -pipechan $errpipechan]] + + # need to use os level channel handle for stdin - try named pipes (or even sockets) instead of fifo2 for this + # If non os-level channel - the command can't be run with the redirection + # stderr/stdout can be run with non-os handles in the call - + # but then it does introduce issues with terminal-detection and behaviour for stdout at least + # + # input is also a tee - we never want to change the source at this point - just log/process a side-channel of it. + # + #set id_in [shellfilter::stack::add $inchan tee_to_pipe -action sink-aside -settings [list -tag commandin -pipechan $inpipechan]] + + + #set id_out [shellfilter::stack::add stdout tee_to_log -action sink-aside -settings [list -tag shellstdout -syslog 127.0.0.1:514 -file ""]] + #set id_err [shellfilter::stack::add stderr tee_to_log -action sink-aside -settings [list -tag shellstderr -syslog 127.0.0.1:514 -file "stderr.txt"]] + + #we need to catch errors - and ensure stack::remove calls occur. + #An error can be raised if the command couldn't even launch, as opposed to a non-zero exitcode and stderr output from the command itself. + # + if {!$is_script} { + set experiment 0 + if {$experiment} { + try { + set results [exec {*}$commandlist] + set exitinfo [list exitcode 0] + } trap CHILDSTATUS {results options} { + set exitcode [lindex [dict get $options -errorcode] 2] + set exitinfo [list exitcode $exitcode] + } + } else { + if {[catch { + #run process with stdout/stderr/stdin or with configured channels + #set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan $inpipechan {*}$opts] + set exitinfo [shellcommand_stdout_stderr $commandlist $outchan $errchan stdin {*}$opts] + #puts stderr "---->exitinfo $exitinfo" + + #subprocess result should usually have an "exitcode" key + #but for background execution we will get a "pids" key of process ids. + } errMsg]} { + set exitinfo [list error "$errMsg" source shellcommand_stdout_stderr] + } + } + } else { + if {[catch { + #script result + set exitinfo [list result [uplevel #0 [list eval $commandlist]]] + } errMsg]} { + set exitinfo [list error "$errMsg" errorCode $::errorCode errorInfo "$::errorInfo"] + } + } + + + #the previous redirections on the underlying inchan/outchan/errchan items will be restored from the -aside setting during removal + #Remove execution-time Tees from stack + shellfilter::stack::remove stdout $id_out + shellfilter::stack::remove stderr $id_err + #shellfilter::stack::remove stderr $id_in + + + #chan configure stderr -buffering line + #flush stdout + + + ::shellfilter::log::write $runtag " return '$exitinfo'" + ::shellfilter::log::close $runtag + return $exitinfo + } + proc ::shellfilter::logtidyup { {tags {}} } { + variable sources + set worker_errorlist [list] + set tidied_sources [list] + set tidytag "logtidy" + + + # opening a thread or writing to a log/syslog close to possible process exit is probably not a great idea. + # we should ensure the thread already exists early on if we really need logging here. + # + #set tid [::shellfilter::log::open $tidytag {-syslog 127.0.0.1:514}] + #::shellfilter::log::write $tidytag " logtidyuptags '$tags'" + + foreach s $sources { + if {$s eq $tidytag} { + continue + } + #puts "logtidyup source $s" + set close 1 + if {[llength $tags]} { + if {$s ni $tags} { + set close 0 + } + } + if {$close} { + lappend tidied_sources $s + shellfilter::log::close $s + lappend worker_errorlist {*}[shellthread::manager::get_and_clear_errors $s] + } + } + set remaining_sources [list] + foreach s $sources { + if {$s ni $tidied_sources} { + lappend remaining_sources $s + } + } + + #set sources [concat $remaining_sources $tidytag] + set sources $remaining_sources + + #shellfilter::stack::unwind stdout + #shellfilter::stack::unwind stderr + return [list tidied $tidied_sources errors $worker_errorlist] + } + + #package require tcl::chan::null + # e.g set errchan [tcl::chan::null] + # e.g chan push stdout [shellfilter::chan::var new ::some_var] + proc ::shellfilter::shellcommand_stdout_stderr {commandlist outchan errchan inchan args} { + set valid_flags [list \ + -timeout \ + -outprefix \ + -errprefix \ + -debug \ + -copytempfile \ + -outbuffering \ + -errbuffering \ + -inbuffering \ + -readprocesstranslation \ + -outtranslation \ + -stdinhandler \ + -outchan \ + -errchan \ + -inchan \ + -teehandle\ + ] + + set runtag shellfilter-run2 + #JMN - load from config + #set tid [::shellfilter::log::open $runtag [list -syslog "127.0.0.1:514"]] + set tid [::shellfilter::log::open $runtag [list -syslog ""]] + + if {[llength $args] % 2} { + error "Trailing arguments after any positional arguments must be in pairs of the form -argname argvalue. Valid flags are:'$valid_flags'" + } + set invalid_flags [list] + foreach {k -} $args { + switch -- $k { + -timeout - + -outprefix - + -errprefix - + -debug - + -copytempfile - + -outbuffering - + -errbuffering - + -inbuffering - + -readprocesstranslation - + -outtranslation - + -stdinhandler - + -outchan - + -errchan - + -inchan - + -teehandle { + } + default { + lappend invalid_flags $k + } + } + } + if {[llength $invalid_flags]} { + error "Unknown option(s)'$invalid_flags': must be one of '$valid_flags'" + } + #line buffering generally best for output channels.. keeps relative output order of stdout/stdin closer to source order + #there may be data where line buffering is inappropriate, so it's configurable per std channel + #reading inputs with line buffering can result in extraneous newlines as we can't detect trailing data with no newline before eof. + set defaults [dict create \ + -outchan stdout \ + -errchan stderr \ + -inchan stdin \ + -outbuffering none \ + -errbuffering none \ + -readprocesstranslation auto \ + -outtranslation lf \ + -inbuffering none \ + -timeout 900000\ + -outprefix ""\ + -errprefix ""\ + -debug 0\ + -copytempfile 0\ + -stdinhandler ""\ + ] + + + + set args [dict merge $defaults $args] + set outbuffering [dict get $args -outbuffering] + set errbuffering [dict get $args -errbuffering] + set inbuffering [dict get $args -inbuffering] + set readprocesstranslation [dict get $args -readprocesstranslation] + set outtranslation [dict get $args -outtranslation] + set timeout [dict get $args -timeout] + set outprefix [dict get $args -outprefix] + set errprefix [dict get $args -errprefix] + set debug [dict get $args -debug] + set copytempfile [dict get $args -copytempfile] + set stdinhandler [dict get $args -stdinhandler] + + set debugname "shellfilter-debug" + + if {$debug} { + set tid [::shellfilter::log::open $debugname [list -syslog "127.0.0.1:514"]] + ::shellfilter::log::write $debugname " commandlist '$commandlist'" + } + #'clock micros' good enough id for shellcommand calls unless one day they can somehow be called concurrently or sequentially within a microsecond and within the same interp. + # a simple counter would probably work too + #consider other options if an alternative to the single vwait in this function is used. + set call_id [tcl::clock::microseconds] ; + set ::shellfilter::shellcommandvars($call_id,exitcode) "" + set waitvar ::shellfilter::shellcommandvars($call_id,waitvar) + if {$debug} { + ::shellfilter::log::write $debugname " waitvar '$waitvar'" + } + lassign [chan pipe] rderr wrerr + chan configure $wrerr -blocking 0 + + set custom_stderr "" + set lastitem [lindex $commandlist end] + #todo - ensure we can handle 2> file (space after >) + + #review - reconsider the handling of redirections such that tcl-style are handled totally separately to other shell syntaxes! + # + #note 2>@1 must ocur as last word for tcl - but 2@stdout can occur elsewhere + #(2>@stdout echoes to main stdout - not into pipeline) + #To properly do pipelines it looks like we will have to split on | and call this proc multiple times and wire it up accordingly (presumably in separate threads) + + switch -- [string trim $lastitem] { + {&} { + set name [lindex $commandlist 0] + #background execution - stdout and stderr from child still comes here - but process is backgrounded + #FIX! - this is broken for paths with backslashes for example + #set pidlist [exec {*}[concat $name [lrange $commandlist 1 end]]] + set pidlist [exec {*}$commandlist] + return [list pids $pidlist] + } + {2>&1} - {2>@1} { + set custom_stderr {2>@1} ;#use the tcl style + set commandlist [lrange $commandlist 0 end-1] + } + default { + # 2> filename + # 2>> filename + # 2>@ openfileid + set redir2test [string range $lastitem 0 1] + if {$redir2test eq "2>"} { + set custom_stderr $lastitem + set commandlist [lrange $commandlist 0 end-1] + } + } + } + set lastitem [lindex $commandlist end] + + set teefile "" ;#empty string, write, append + #an ugly hack.. because redirections seem to arrive wrapped - review! + #There be dragons here.. + #Be very careful with list manipulation of the commandlist string.. backslashes cause havoc. commandlist must always be a well-formed list. generally avoid string manipulations on entire list or accidentally breaking a list element into parts if it shouldn't be.. + #The problem here - is that we can't always know what was intended on the commandline regarding quoting + + ::shellfilter::log::write $runtag "checking for redirections in $commandlist" + #sometimes we see a redirection without a following space e.g >C:/somewhere + #normalize + switch -regexp -- $lastitem\ + {^>[/[:alpha:]]+} { + set lastitem "> [string range $lastitem 1 end]" + }\ + {^>>[/[:alpha:]]+} { + set lastitem ">> [string range $lastitem 2 end]" + } + + + #for a redirection, we assume either a 2-element list at tail of form {> {some path maybe with spaces}} + #or that the tail redirection is not wrapped.. x y z > {some path maybe with spaces} + #we can't use list methods such as llenth on a member of commandlist + set wordlike_parts [regexp -inline -all {\S+} $lastitem] + + if {([llength $wordlike_parts] >= 2) && ([lindex $wordlike_parts 0] in [list ">>" ">"])} { + #wrapped redirection - but maybe not 'well' wrapped (unquoted filename) + set lastitem [string trim $lastitem] ;#we often see { > something} + + #don't use lassign or lrange on the element itself without checking first + #we can treat the commandlist as a whole as a well formed list but not neccessarily each element within. + #lassign $lastitem redir redirtarget + #set commandlist [lrange $commandlist 0 end-1] + # + set itemchars [split $lastitem ""] + set firstchar [lindex $itemchars 0] + set lastchar [lindex $itemchars end] + + #NAIVE test for double quoted only! + #consider for example {"a" x="b"} + #testing first and last is not decisive + #We need to decide what level of drilling down is even appropriate here.. + #if something was double wrapped - it was perhaps deliberate so we don't interpret it as something(?) + set head_tail_chars [list $firstchar $lastchar] + set doublequoted [expr {[llength [lsearch -all $head_tail_chars "\""]] == 2}] + if {[string equal "\{" $firstchar] && [string equal "\}" $lastchar]} { + set curlyquoted 1 + } else { + set curlyquoted 0 + } + + if {$curlyquoted} { + #these are not the tcl protection brackets but ones supplied in the argument + #it's still not valid to use list operations on a member of the commandlist + set inner [string range $lastitem 1 end-1] + #todo - fix! we still must assume there could be list-breaking data! + set innerwords [regexp -inline -all {\S+} $inner] ;#better than [split $inner] because we don't get extra empty elements for each whitespace char + set redir [lindex $innerwords 0] ;#a *potential* redir - to be tested below + set redirtarget [lrange $innerwords 1 end] ;#all the rest + } elseif {$doublequoted} { + ::shellfilter::log::write $debugname "doublequoting at tail of command '$commandlist'" + set inner [string range $lastitem 1 end-1] + set innerwords [regexp -inline -all {\S+} $inner] + set redir [lindex $innerwords 0] + set redirtarget [lrange $innerwords 1 end] + } else { + set itemwords [regexp -inline -all {\S+} $lastitem] + # e.g > c:\test becomes > {c:\test} + # but > c/mnt/c/test/temp.txt stays as > /mnt/c/test/temp.txt + set redir [lindex $itemwords 0] + set redirtarget [lrange $itemwords 1 end] + } + set commandlist [lrange $commandlist 0 end-1] + + } elseif {[lindex $commandlist end-1] in [list ">>" ">"]} { + #unwrapped redirection + #we should be able to use list operations like lindex and lrange here as the command itself is hopefully still a well formed list + set redir [lindex $commandlist end-1] + set redirtarget [lindex $commandlist end] + set commandlist [lrange $commandlist 0 end-2] + } else { + #no redirection + set redir "" + set redirtarget "" + #no change to command list + } + + + switch -- $redir { + ">>" - ">" { + set redirtarget [string trim $redirtarget "\""] + ::shellfilter::log::write $runtag " have redirection '$redir' to '$redirtarget'" + + set winfile $redirtarget ;#default assumption + switch -glob -- $redirtarget { + "/c/*" { + set winfile "c:/[string range $redirtarget 3 end]" + } + "/mnt/c/*" { + set winfile "c:/[string range $redirtarget 7 end]" + } + } + + if {[file exists [file dirname $winfile]]} { + #containing folder for target exists + if {$redir eq ">"} { + set teefile "write" + } else { + set teefile "append" + } + ::shellfilter::log::write $runtag "Directory exists '[file dirname $winfile]' operation:$teefile" + } else { + #we should be writing to a file.. but can't + ::shellfilter::log::write $runtag "cannot verify directory exists '[file dirname $winfile]'" + } + } + default { + ::shellfilter::log::write $runtag "No redir found!!" + } + } + + #often first element of command list is wrapped and cannot be run directly + #e.g {{ls -l} {> {temp.tmp}}} + #we will assume that if there is a single element which is a pathname containing a space - it is doubly wrapped. + # this may not be true - and the command may fail if it's just {c:\program files\etc} but it is the less common case and we currently have no way to detect. + #unwrap first element.. will not affect if not wrapped anyway (subject to comment above re spaces) + set commandlist [concat [lindex $commandlist 0] [lrange $commandlist 1 end]] + + #todo? + #child process environment. + # - to pass a different environment to the child - we would need to save the env array, modify as required, and then restore the env array. + + #to restore buffering states after run + set remember_in_out_err_buffering [list \ + [chan configure $inchan -buffering] \ + [chan configure $outchan -buffering] \ + [chan configure $errchan -buffering] \ + ] + + set remember_in_out_err_translation [list \ + [chan configure $inchan -translation] \ + [chan configure $outchan -translation] \ + [chan configure $errchan -translation] \ + ] + + + + + + chan configure $inchan -buffering $inbuffering -blocking 0 ;#we are setting up a readable handler for this - so non-blocking ok + chan configure $errchan -buffering $errbuffering + #chan configure $outchan -blocking 0 + chan configure $outchan -buffering $outbuffering ;#don't configure non-blocking. weird duplicate of *second* line occurs if you do. + # + + #-------------------------------------------- + #Tested on windows. Works to stop in output when buffering is none, reading from channel with -translation auto + #cmd, pwsh, tcl + #chan configure $outchan -translation lf + #chan configure $errchan -translation lf + #-------------------------------------------- + chan configure $outchan -translation $outtranslation + chan configure $errchan -translation $outtranslation + + #puts stderr "chan configure $wrerr [chan configure $wrerr]" + if {$debug} { + ::shellfilter::log::write $debugname "COMMAND [list $commandlist] strlen:[string length $commandlist] llen:[llength $commandlist]" + } + #todo - handle custom redirection of stderr to a file? + if {[string length $custom_stderr]} { + #::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist $custom_stderr] a+" + #set rdout [open |[concat $commandlist $custom_stderr] a+] + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list $custom_stderr <@$inchan]] [list RDONLY]" + set rdout [open |[concat $commandlist [list <@$inchan $custom_stderr]] [list RDONLY]] + set rderr "bogus" ;#so we don't wait for it + } else { + ::shellfilter::log::write $runtag "LAUNCH open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]" + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] a+] + #set rdout [open |[concat $commandlist [list 2>@$wrerr]] [list RDWR]] + + # If we don't redirect stderr to our own tcl-based channel - then the transforms don't get applied. + # This is the whole reason we need these file-event loops. + # Ideally we need something like exec,open in tcl that interacts with transformed channels directly and emits as it runs, not only at termination + # - and that at least appears like a terminal to the called command. + #set rdout [open |[concat $commandlist [list 2>@stderr <@$inchan]] [list RDONLY]] + + + set rdout [open |[concat $commandlist [list 2>@$wrerr <@$inchan]] [list RDONLY]] + + chan configure $rderr -buffering $errbuffering -blocking 0 + chan configure $rderr -translation $readprocesstranslation + } + + + + set command_pids [pid $rdout] + #puts stderr "command_pids: $command_pids" + #tcl::process ensemble only available in 8.7+ - and it didn't prove useful here anyway + # the child process generally won't shut down until channels are closed. + # premature EOF on grandchild process launch seems to be due to lack of terminal emulation when redirecting stdin/stdout. + # worked around in punk/repl using 'script' command as a fake tty. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $command_pids 0] ni $subprocesses} { + # puts stderr "pid [lindex $command_pids 0] not running $errMsg" + #} else { + # puts stderr "pid [lindex $command_pids 0] is running" + #} + + + if {$debug} { + ::shellfilter::log::write $debugname "pipeline pids: $command_pids" + } + + #jjj + + + chan configure $rdout -buffering $outbuffering -blocking 0 + chan configure $rdout -translation $readprocesstranslation + + if {![string length $custom_stderr]} { + chan event $rderr readable [list apply {{chan other wrerr outchan errchan waitfor errprefix errbuffering debug debugname pids} { + if {$errbuffering eq "line"} { + set countchunk [chan gets $chan chunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #errprefix only applicable to line buffered output + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $errchan ${errprefix}$chunk + } else { + puts $errchan "${errprefix}$chunk" + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $errchan $chunk + } + } + if {[chan eof $chan]} { + flush $errchan ;#jmn + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stderr reader: pid [lindex $pids 0] no longer running" + #} else { + # puts stderr "stderr reader: pid [lindex $pids 0] still running" + #} + chan close $chan + #catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stderr + } + } + }} $rderr $rdout $wrerr $outchan $errchan $waitvar $errprefix $errbuffering $debug $debugname $command_pids] + } + + #todo - handle case where large amount of stdin coming in faster than rdout can handle + #as is - arbitrary amount of memory could be used because we aren't using a filevent for rdout being writable + # - we're just pumping it in to the non-blocking rdout buffers + # ie there is no backpressure and stdin will suck in as fast as possible. + # for most commandlines this probably isn't too big a deal.. but it could be a problem for multi-GB disk images etc + # + # + + ## Note - detecting trailing missing nl before eof is basically the same here as when reading rdout from executable + # - but there is a slight difference in that with rdout we get an extra blocked state just prior to the final read. + # Not known if that is significant + ## with inchan configured -buffering line + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl -r cat + #warning reading input with -buffering line. Cannot detect missing trailing-newline at eof + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:1 pend:-1 count:3 + #etc + + if 0 { + chan event $inchan readable [list apply {{chan wrchan inbuffering waitfor} { + #chan copy stdin $chan ;#doesn't work in a chan event + if {$inbuffering eq "line"} { + set countchunk [chan gets $chan chunk] + #puts $wrchan "stdinstate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {[chan eof $chan]} { + puts -nonewline $wrchan $chunk + } else { + puts $wrchan $chunk + } + } + } else { + set chunk [chan read $chan] + if {[string length $chunk]} { + puts -nonewline $wrchan $chunk + } + } + if {[chan eof $chan]} { + puts stderr "|stdin_reader>eof [chan configure stdin]" + chan event $chan readable {} + #chan close $chan + chan close $wrchan write ;#half close + #set $waitfor "stdin" + } + }} $inchan $rdout $inbuffering $waitvar] + + if {[string length $stdinhandler]} { + chan configure stdin -buffering line -blocking 0 + chan event stdin readable $stdinhandler + } + } + + set actual_proc_out_buffering [chan configure $rdout -buffering] + set actual_outchan_buffering [chan configure $outchan -buffering] + #despite whatever is configured - we match our reading to how we need to output + set read_proc_out_buffering $actual_outchan_buffering + + + + if {[string length $teefile]} { + set logname "redir_[string map {: _} $winfile]_[tcl::clock::microseconds]" + set tid [::shellfilter::log::open $logname {-syslog 127.0.0.1:514}] + if {$teefile eq "write"} { + ::shellfilter::log::write $logname "opening '$winfile' for write" + set fd [open $winfile w] + } else { + ::shellfilter::log::write $logname "opening '$winfile' for appending" + set fd [open $winfile a] + } + #chan configure $fd -translation lf + chan configure $fd -translation $outtranslation + chan configure $fd -encoding utf-8 + + set tempvar_bytetotal [namespace current]::totalbytes[tcl::clock::microseconds] + set $tempvar_bytetotal 0 + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname writefile writefilefd copytempfile bytevar logtag} { + #review - if we write outprefix to normal stdout.. why not to redirected file? + #usefulness of outprefix is dubious + upvar $bytevar totalbytes + if {$read_proc_out_buffering eq "line"} { + #set outchunk [chan read $chan] + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + if {$countchunk >= 0} { + if {![chan eof $chan]} { + set numbytes [expr {[string length $outchunk] + 1}] ;#we are assuming \n not \r\n - but count won't/can't be completely accurate(?) - review + puts $writefilefd $outchunk + } else { + set numbytes [string length $outchunk] + puts -nonewline $writefilefd $outchunk + } + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + #puts $outchan "${outprefix} wrote $numbytes bytes to $writefile" + } + } else { + set outchunk [chan read $chan] + if {[string length $outchunk]} { + puts -nonewline $writefilefd $outchunk + set numbytes [string length $outchunk] + incr totalbytes $numbytes + ::shellfilter::log::write $logtag "${outprefix} wrote $numbytes bytes to $writefile" + } + } + if {[chan eof $chan]} { + flush $writefilefd ;#jmn + #set blocking so we can get exit code + chan configure $chan -blocking 1 + catch {::shellfilter::log::write $logtag "${outprefix} total bytes $totalbytes written to $writefile"} + #puts $outchan "${outprefix} total bytes $totalbytes written to $writefile" + catch {close $writefilefd} + if {$copytempfile} { + catch {file copy $writefile "[file rootname $writefile]_copy[file extension $writefile]"} + } + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {$debug} { + ::shellfilter::log::write $debugname "(teefile) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $winfile $fd $copytempfile $tempvar_bytetotal $logname] + + } else { + + # This occurs when we have outbuffering set to 'line' - as the 'input' from rdout which comes from the executable is also configured to 'line' + # where b:0|1 is whether chan blocked $chan returns 0 or 1 + # pend is the result of chan pending $chan + # eof is the resot of chan eof $chan + + + ##------------------------- + ##If we still read with gets,to retrieve line by line for output to line-buffered output - but the input channel is configured with -buffering none + ## then we can detect the difference + # there is an extra blocking read - but we can stil use eof with data to detect the absent newline and avoid passing an extra one on. + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /u/c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:1 eof:0 pend:-1 count:-1 + #instate b:0 eof:1 pend:-1 count:3 + #etc + ##------------------------ + + + #this should only occur if upstream is coming from stdin reader that has line buffering and hasn't handled the difference properly.. + ###reading with gets from line buffered input with trailing newline + #c:\repo\jn\punk\test>printf "test\netc\n" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + + ###reading with gets from line buffered input with trailing newline + ##No detectable difference! + #c:\repo\jn\punk\test>printf "test\netc" | tclsh punk.vfs/main.tcl /c cat + #instate b:0 eof:0 pend:-1 count:4 + #test + #instate b:0 eof:0 pend:-1 count:3 + #etc + #instate b:0 eof:1 pend:-1 count:-1 + ##------------------------- + + #Note that reading from -buffering none and writing straight out gives no problem because we pass the newlines through as is + + + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($rdout) 0 ;#a very specific case of readblocked prior to eof.. possibly not important + #this detection is disabled for now - but left for debugging in case it means something.. or changes + chan event $rdout readable [list apply {{chan other wrerr outchan errchan read_proc_out_buffering waitfor outprefix call_id debug debugname pids} { + #set outchunk [chan read $chan] + + if {$read_proc_out_buffering eq "line"} { + set countchunk [chan gets $chan outchunk] ;#only get one line so that order between stderr and stdout is more likely to be preserved + #countchunk can be -1 before eof e.g when blocked + #debugging output inline with data - don't leave enabled + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + if {$countchunk >= 0} { + if {![chan eof $chan]} { + puts $outchan ${outprefix}$outchunk + } else { + puts -nonewline $outchan ${outprefix}$outchunk + #if {$::shellfilter::chan::lastreadblocked_nodata_noeof($chan)} { + # seems to be the usual case + #} else { + # #false alarm, or ? we've reached eof with data but didn't get an empty blocking read just prior + # #Not known if this occurs + # #debugging output inline with data - don't leave enabled + # puts $outchan "!!!prev read didn't block: instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:$countchunk" + #} + } + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) 0 + } else { + #set ::shellfilter::chan::lastreadblocked_nodata_noeof($chan) [expr {[chan blocked $chan] && ![chan eof $chan]}] + } + } else { + #puts $outchan "read CHANNEL $chan [chan configure $chan]" + #puts $outchan "write CHANNEL $outchan b:[chan configure $outchan -buffering] t:[chan configure $outchan -translation] e:[chan configure $outchan -encoding]" + set outchunk [chan read $chan] + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan] count:[string length $outchunk]" + if {[string length $outchunk]} { + #set stringrep [encoding convertfrom utf-8 $outchunk] + #set newbytes [encoding convertto utf-16 $stringrep] + #puts -nonewline $outchan $newbytes + puts -nonewline $outchan $outchunk + } + } + + if {[chan eof $chan]} { + flush $outchan ;#jmn + #for now just look for first element in the pid list.. + #set subprocesses [tcl::process::list] + #puts stderr "subprocesses: $subprocesses" + #if {[lindex $pids 0] ni $subprocesses} { + # puts stderr "stdout reader pid: [lindex $pids 0] no longer running" + #} else { + # puts stderr "stdout reader pid: [lindex $pids 0] still running" + #} + + #puts $outchan "instate b:[chan blocked $chan] eof:[chan eof $chan] pend:[chan pending output $chan]" + chan configure $chan -blocking 1 ;#so we can get exit code + try { + chan close $chan + set ::shellfilter::shellcommandvars($call_id,exitcode) 0 + if {$debug} { + ::shellfilter::log::write $debugname " -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + set ::shellfilter::shellcommandvars($call_id,exitcode) $code + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with code: $code" + } + } trap CHILDKILLED {result options} { + #set code [lindex [dict get $options -errorcode] 2] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled" + if {$debug} { + ::shellfilter::log::write $debugname " CHILD PROCESS EXITED with result:'$result' options:'$options'" + } + + } finally { + #puts stdout "HERE" + #flush stdout + + } + catch {chan close $wrerr} + if {$other ni [chan names]} { + set $waitfor stdout + } + + } + }} $rdout $rderr $wrerr $outchan $errchan $read_proc_out_buffering $waitvar $outprefix $call_id $debug $debugname $command_pids] + } + + #todo - add ability to detect activity/data-flow and change timeout to only apply for period with zero data + #e.g x hrs with no data(?) + #reset timeout when data detected. + after $timeout [string map [list %w% $waitvar %id% $call_id %wrerr% $wrerr %rdout% $rdout %rderr% $rderr %debug% $debug %debugname% $debugname] { + if {[info exists ::shellfilter::shellcommandvars(%id%,exitcode)]} { + if {[set ::shellfilter::shellcommandvars(%id%,exitcode)] ne ""} { + catch { chan close %wrerr% } + catch { chan close %rdout%} + catch { chan close %rderr%} + } else { + chan configure %rdout% -blocking 1 + try { + chan close %rdout% + set ::shellfilter::shellcommandvars(%id%,exitcode) 0 + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) -- child process returned no error. (exit code 0) --" + } + } trap CHILDSTATUS {result options} { + set code [lindex [dict get $options -errorcode] 2] + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILD PROCESS EXITED with code: $code" + } + set ::shellfilter::shellcommandvars(%id%,exitcode) $code + } trap CHILDKILLED {result options} { + set code [lindex [dict get $options -errorcode] 2] + #set code [dict get $options -code] + #set ::shellfilter::shellcommandvars(%id%,exitcode) $code + #set ::shellfilter::shellcommandvars($call_id,exitcode) "childkilled-timeout" + set ::shellfilter::shellcommandvars(%id%,exitcode) "childkilled-timeout" + if {%debug%} { + ::shellfilter::log::write %debugname% "(timeout) CHILDKILLED with code: $code" + ::shellfilter::log::write %debugname% "(timeout) result:$result options:$options" + } + + } + catch { chan close %wrerr% } + catch { chan close %rderr%} + } + set %w% "timeout" + } + }] + + + vwait $waitvar + + set exitcode [set ::shellfilter::shellcommandvars($call_id,exitcode)] + if {![string is digit -strict $exitcode]} { + puts stderr "Process exited with non-numeric code: $exitcode" + flush stderr + } + if {[string length $teefile]} { + #cannot be called from within an event handler above.. vwait reentrancy etc + catch {::shellfilter::log::close $logname} + } + + if {$debug} { + ::shellfilter::log::write $debugname " closed by: [set $waitvar] with exitcode: $exitcode" + catch {::shellfilter::log::close $debugname} + } + array unset ::shellfilter::shellcommandvars $call_id,* + + + #restore buffering to pre shellfilter::run state + lassign $remember_in_out_err_buffering bin bout berr + chan configure $inchan -buffering $bin + chan configure $outchan -buffering $bout + chan configure $errchan -buffering $berr + + lassign $remember_in_out_err_translation tin tout terr + chan configure $inchan -translation $tin + chan configure $outchan -translation $tout + chan configure $errchan -translation $terr + + + #in channel probably closed..(? review - should it be?) + catch { + chan configure $inchan -buffering $bin + } + + + return [list exitcode $exitcode] + } + +} + +package provide shellfilter [namespace eval shellfilter { + variable version + set version 0.2 +}] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm index 31995bfe..d9858980 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/textblock-0.1.3.tm @@ -137,11 +137,31 @@ tcl::namespace::eval textblock { return " -choices \{$choices\} -help {algorithm choice $choicemsg} " } } + namespace eval argdoc { + tcl::namespace::import ::punk::ansi::a+ + # -- --- --- --- --- + #non colour SGR codes + # we can use these directly via ${$I} etc without marking a definition with @dynamic + #This is because they don't need to change when colour switched on and off. + set I [a+ italic] + set NI [a+ noitalic] + set B [a+ bold] + set N [a+ normal] + # -- --- --- --- --- + proc example {str} { + set str [string trimleft $str \n] + set block [punk::ansi::ansiwrap Web-gray [textblock::frame -ansibase [a+ Web-gray bold white] -ansiborder [a+ black White] -boxlimits {hl} -type block $str]] + set result [textblock::bookend_lines $block [a] "[a defaultbg] [a]"] + #puts $result + return $result + } + } + # hash_algorithm -optional 1 -choices {${[::textblock::argdoc::hash_algorithm_choices]}} -help\ # "algorithm choice" namespace eval argdoc { - set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {$[::textblock::argdoc::hash_algorithm_choices_and_help]} + set DYN_HASH_ALGORITHM_CHOICES_AND_HELP {${[::textblock::argdoc::hash_algorithm_choices_and_help]}} punk::args::define { @dynamic @id -id ::textblock::use_hash @@ -154,7 +174,6 @@ tcl::namespace::eval textblock { } } proc use_hash {args} { - #set argd [punk::args::get_by_id ::textblock::use_hash $args] set argd [punk::args::parse $args withid ::textblock::use_hash] variable use_hash if {![dict exists $argd received hash_algorithm]} { @@ -2294,7 +2313,8 @@ tcl::namespace::eval textblock { #JMN #spanned_parts are all built with textblock::frame - therefore uniform-width lines - can use join_basic - set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + #set spanned_frame [textblock::join_basic -- {*}$spanned_parts] + set spanned_frame [textblock::join_basic_raw {*}$spanned_parts] if {$spans_to_rhs} { if {$cidx == 0} { @@ -2363,7 +2383,8 @@ tcl::namespace::eval textblock { } else { #this_span == 1 - set spanned_frame [textblock::join_basic -- $header_cell_startspan] + #set spanned_frame [textblock::join_basic -- $header_cell_startspan] + set spanned_frame [textblock::join_basic_raw $header_cell_startspan] } @@ -3992,7 +4013,8 @@ tcl::namespace::eval textblock { set body_build "" } else { #body blocks should not be ragged - so can use join_basic - set body_build [textblock::join_basic -- {*}$body_blocks] + #set body_build [textblock::join_basic -- {*}$body_blocks] + set body_build [textblock::join_basic_raw {*}$body_blocks] } if {$headerheight > 0} { set table [tcl::string::cat $header_build \n $body_build] @@ -4149,7 +4171,6 @@ tcl::namespace::eval textblock { proc periodic {args} { #For an impressive interactive terminal app (javascript) # see: https://github.com/spirometaxas/periodic-table-cli - #set opts [dict get [punk::args::get_by_id ::textblock::periodic $args] opts] set opts [dict get [punk::args::parse $args withid ::textblock::periodic] opts] set opt_return [tcl::dict::get $opts -return] if {[tcl::dict::get $opts -forcecolour]} { @@ -4446,7 +4467,7 @@ tcl::namespace::eval textblock { proc list_as_table {args} { set FRAMETYPES [textblock::frametypes] - set argd [punk::args::get_by_id ::textblock::list_as_table $args] + set argd [punk::args::parse $args withid ::textblock::list_as_table] set opts [dict get $argd opts] set received [dict get $argd received] @@ -4644,7 +4665,8 @@ tcl::namespace::eval textblock { if {[tcl::string::last \n $charblock] >= 0} { if {$blockwidth > 1} { #set row [.= val $charblock {*}[lrepeat [expr {$blockwidth -1}] |> piper_blockjoin $charblock]] ;#building a repeated "|> command arg" list to evaluate as a pipeline. (from before textblock::join could take arbitrary num of blocks ) - set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + #set row [textblock::join_basic -- {*}[lrepeat $blockwidth $charblock]] + set row [textblock::join_basic_raw {*}[lrepeat $blockwidth $charblock]] } else { set row $charblock } @@ -4694,7 +4716,7 @@ tcl::namespace::eval textblock { } proc testblock {args} { - set argd [punk::args::get_by_id ::textblock::testblock $args] + set argd [punk::args::parse $args withid ::textblock::testblock] set colour [dict get $argd values colour] set size [dict get $argd opts -size] @@ -4762,7 +4784,8 @@ tcl::namespace::eval textblock { if {"noreset" in $colour} { return [textblock::join_basic -ansiresets 0 -- {*}$clist] } else { - return [textblock::join_basic -- {*}$clist] + #return [textblock::join_basic -- {*}$clist] + return [textblock::join_basic_raw {*}$clist] } } elseif {"rainbow" in $colour} { #direction must be horizontal @@ -5019,19 +5042,20 @@ tcl::namespace::eval textblock { -width ""\ -overflow 0\ -within_ansi 0\ + -return block\ ] #known_samewidth of empty string means we don't know either way, 0 is definitely 'ragged', 1 is definitely homogenous #review!? #-within_ansi means after a leading ansi code when doing left pad on all but last line #-within_ansi means before a trailing ansi code when doing right pad on all but last line - set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0?" foreach {k v} $args { switch -- $k { - -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi { + -padchar - -which - -known_hasansi - -known_samewidth - -known_blockwidth - -width - -overflow - -within_ansi - -return { tcl::dict::set opts $k $v } default { + set usage "pad block ?-padchar |? ?-which right|left|centre? ?-known_hasansi \"\"|? ?-known_blockwidth \"\"|? ?-width auto|? ?-within_ansi 1|0? ?-return block|list?" error "textblock::pad unrecognised option '$k'. Usage: $usage" } } @@ -5177,96 +5201,110 @@ tcl::namespace::eval textblock { set line_len 0 set pad_cache [dict create] ;#key on value of 'missing' - which is width of required pad foreach {pt ansi} $parts { - if {$pt ne ""} { - set has_nl [expr {[tcl::string::last \n $pt]>=0}] - if {$has_nl} { + if {$pt eq ""} { + #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes + lappend line_chunks "" + } elseif {[tcl::string::last \n $pt]==-1} { + lappend line_chunks $pt + if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + incr line_len [punk::char::grapheme_width_cached $pt] ;#memleak - REVIEW + } + } else { + #set has_nl [expr {[tcl::string::last \n $pt]>=0}] + #if {$has_nl} { set pt [tcl::string::map [list \r\n \n] $pt] set partlines [split $pt \n] - } else { - set partlines [list $pt] - } - set last [expr {[llength $partlines]-1}] - set p 0 - foreach pl $partlines { - lappend line_chunks $pl + #} else { + # set partlines [list $pt] + #} + #set last [expr {[llength $partlines]-1}] + #set p -1 + foreach pl [lrange $partlines 0 end-1] { + #incr p + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. #incr line_len [punk::char::ansifreestring_width $pl] + #if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { + # incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + #} + #do padding if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + set missing [expr {$width - $line_len}] + } else { + set missing [expr {$width - $datawidth}] } - if {$p != $last} { - #do padding - if {$known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq ""} { - set missing [expr {$width - $line_len}] - } else { - set missing [expr {$width - $datawidth}] - } - if {$missing > 0} { - #commonly in a block - many lines will have the same pad - cache based on missing + if {$missing > 0} { + #commonly in a block - many lines will have the same pad - cache based on missing - #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars - if {[tcl::dict::exists $pad_cache $missing]} { - set pad [tcl::dict::get $pad_cache $missing] + #padchar may be more than 1 wide - because of 2wide unicode and or multiple chars + if {[tcl::dict::exists $pad_cache $missing]} { + set pad [tcl::dict::get $pad_cache $missing] + } else { + set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width + if {!$pad_has_ansi} { + set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] } else { - set repeats [expr {int(ceil($missing / double($padcharsize)))}] ;#will overshoot by 1 whenever padcharsize not an exact divisor of width - if {!$pad_has_ansi} { - set pad [tcl::string::range [tcl::string::repeat $padchar $repeats] 0 $missing-1] - } else { - set base [tcl::string::repeat " " $missing] - set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] - } - dict set pad_cache $missing $pad + set base [tcl::string::repeat " " $missing] + set pad [overtype::block -blockalign left -overflow 0 $base [tcl::string::repeat $padchar $repeats]] } - switch -- $which-$opt_withinansi { - r-0 { - lappend line_chunks $pad - } - r-1 { - if {[lindex $line_chunks end] eq ""} { - set line_chunks [linsert $line_chunks end-2 $pad] - } else { - lappend line_chunks $pad - } - } - r-2 { + dict set pad_cache $missing $pad + } + switch -- $which-$opt_withinansi { + r-0 { + lappend line_chunks $pad + } + r-1 { + if {[lindex $line_chunks end] eq ""} { + set line_chunks [linsert $line_chunks end-2 $pad] + } else { lappend line_chunks $pad } - l-0 { - set line_chunks [linsert $line_chunks 0 $pad] + } + r-2 { + lappend line_chunks $pad + } + l-0 { + set line_chunks [linsert $line_chunks 0 $pad] + } + l-1 { + if {[lindex $line_chunks 0] eq ""} { + set line_chunks [linsert $line_chunks 2 $pad] + } else { + set line_chunks [linsert $line_chunks 0 $pad] } - l-1 { + } + l-2 { + if {$lnum == 0} { if {[lindex $line_chunks 0] eq ""} { set line_chunks [linsert $line_chunks 2 $pad] } else { set line_chunks [linsert $line_chunks 0 $pad] } - } - l-2 { - if {$lnum == 0} { - if {[lindex $line_chunks 0] eq ""} { - set line_chunks [linsert $line_chunks 2 $pad] - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } - } else { - set line_chunks [linsert $line_chunks 0 $pad] - } + } else { + set line_chunks [linsert $line_chunks 0 $pad] } } } - lappend lines [::join $line_chunks ""] - set line_chunks [list] - set line_len 0 - incr lnum } - incr p + lappend lines [::join $line_chunks ""] + set line_chunks [list] + set line_len 0 + incr lnum } - } else { - #we need to store empties in order to insert text in the correct position relative to leading/trailing ansi codes - lappend line_chunks "" + #deal with last part zzz of xxx\nyyy\nzzz - not yet a complete line + set pl [lindex $partlines end] + lappend line_chunks $pl ;#we need to lappend because there can already be some pt and ansi entries for the current line from previous {pt ansi} values where pt had no newline. + if {$pl ne "" && ($known_samewidth eq "" || ($known_samewidth ne "" && !$known_samewidth) || $datawidth eq "")} { + incr line_len [punk::char::grapheme_width_cached $pl] ;#memleak - REVIEW + } + } #don't let trailing empty ansi affect the line_chunks length if {$ansi ne ""} { - lappend line_chunks $ansi ;#don't update line_len - review - ansi codes with visible content? + lappend line_chunks $ansi ;#don't update line_len + #- review - ansi codes with visible content? + #- There shouldn't be any, even though for example some terminals display PM content + #e.g OSC 8 is ok as it has the uri 'inside' the ansi sequence, but that's ok because the displayable part is outside and is one of our pt values from split_codes. } } #pad last line @@ -5325,7 +5363,11 @@ tcl::namespace::eval textblock { } } lappend lines [::join $line_chunks ""] - return [::join $lines \n] + if {[tcl::dict::get $opts -return] eq "block"} { + return [::join $lines \n] + } else { + return $lines + } } #left insertion into a list resulting from punk::ansi::ta::split_codes or split_codes_single @@ -5566,7 +5608,7 @@ tcl::namespace::eval textblock { #join without regard to each line length in a block (no padding added to make each block uniform) proc ::textblock::join_basic {args} { - set argd [punk::args::get_by_id ::textblock::join_basic $args] + set argd [punk::args::parse $args withid ::textblock::join_basic] set ansiresets [tcl::dict::get $argd opts -ansiresets] set blocks [tcl::dict::get $argd values blocks] @@ -5602,6 +5644,33 @@ tcl::namespace::eval textblock { } return [::join $outlines \n] } + proc ::textblock::join_basic_raw {args} { + #no options. -*, -- are legimate blocks + set blocklists [lrepeat [llength $args] ""] + set blocklengths [lrepeat [expr {[llength $args]+1}] 0] ;#add 1 to ensure never empty - used only for rowcount max calc + set i -1 + foreach b $args { + incr i + if {[punk::ansi::ta::detect $b]} { + #-ansireplays 1 quite expensive e.g 7ms in 2024 + set blines [punk::lib::lines_as_list -ansireplays 1 -ansiresets auto -- $b] + } else { + set blines [split $b \n] + } + lset blocklengths $i [llength $blines] + lset blocklists $i $blines + } + set rowcount [tcl::mathfunc::max {*}$blocklengths] + set outlines [lrepeat $rowcount ""] + for {set r 0} {$r < $rowcount} {incr r} { + set row "" + foreach blines $blocklists { + append row [lindex $blines $r] + } + lset outlines $r $row + } + return [::join $outlines \n] + } proc ::textblock::join_basic2 {args} { #@cmd -name textblock::join_basic -help "Join blocks line by line but don't add padding on each line to enforce uniform width. # Already uniform blocks will join faster than textblock::join, and ragged blocks will join in a ragged manner @@ -5686,9 +5755,12 @@ tcl::namespace::eval textblock { } set idx 0 - set blocklists [list] + #set blocklists [list] + set blocklists [lrepeat [llength $blocks] ""] set rowcount 0 + set bidx -1 foreach b $blocks { + incr bidx #we need the width of a rendered block for per-row renderline calls or padding #we may as well use widthinfo to also determine raggedness state to pass on to pad function #set bwidth [width $b] @@ -5705,18 +5777,21 @@ tcl::namespace::eval textblock { if {[punk::ansi::ta::detect $b]} { # - we need to join to use pad - even though we then need to immediately resplit REVIEW (make line list version of pad?) set replay_block [::join [punk::lib::lines_as_list -ansireplays 1 -ansiresets $ansiresets -- $b] \n] - set bl [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $replay_block -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $replay_block -return lines -known_hasansi 1 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } else { #each block is being rendered into its own empty column - we don't need resets if it has no ansi, even if blocks to left and right do have ansi - set bl [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + #set blines [split [textblock::pad $b -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] \n] + set blines [textblock::pad $b -return lines -known_hasansi 0 -known_samewidth $is_samewidth -known_blockwidth $bwidth -width $bwidth -which right -padchar " "] } - set rowcount [expr {max($rowcount,[llength $bl])}] - lappend blocklists $bl + set rowcount [expr {max($rowcount,[llength $blines])}] + #lappend blocklists $bl + lset blocklists $bidx $blines set width($idx) $bwidth incr idx } - set outlines [list] + set outlines [lrepeat $rowcount ""] for {set r 0} {$r < $rowcount} {incr r} { set row "" for {set c 0} {$c < [llength $blocklists]} {incr c} { @@ -5726,7 +5801,8 @@ tcl::namespace::eval textblock { } append row $cell } - lappend outlines $row + #lappend outlines $row + lset outlines $r $row } return [::join $outlines \n] } @@ -5910,7 +5986,7 @@ tcl::namespace::eval textblock { set table [[textblock::spantest] print] set punks [a+ web-lawngreen][>punk . lhs][a]\n\n[a+ rgb#FFFF00][>punk . rhs][a] set ipunks [overtype::renderspace -width [textblock::width $punks] [punk::ansi::enable_inverse]$punks] - set testblock [textblock::testblock 15 rainbow] + set testblock [textblock::testblock -size 15 rainbow] set contents $ansi\n[textblock::join -- " " $table " " $punks " " $testblock " " $ipunks " " $punks] set framed [textblock::frame -checkargs 0 -type arc -title [a+ cyan]Compositing[a] -subtitle [a+ red]ANSI[a] -ansiborder [a+ web-orange] $contents] } @@ -6206,9 +6282,11 @@ tcl::namespace::eval textblock { set spec [string map [list $::textblock::frametypes] { @id -id ::textblock::framedef @cmd -name textblock::framedef\ + -summary "Return frame graphical elements as a dictionary."\ -help "Return a dict of the elements that make up a frame border. May return a subset of available elements based on memberglob values." - + @leaders -min 0 -max 0 + @opts -joins -default "" -type list\ -help "List of join directions, any of: up down left right or those combined with another frametype e.g left-heavy down-light." @@ -6216,7 +6294,7 @@ tcl::namespace::eval textblock { -help "-boxonly true restricts results to the corner,vertical and horizontal box elements It excludes the extra top and side join elements htlj,hlbj,vllj,vlrj." - @values -min 1 + @values -min 1 -max -1 frametype -choices "" -choiceprefix 0 -choicerestricted 0 -type dict\ -help "name from the predefined frametypes or an adhoc dictionary." memberglob -type globstring -optional 1 -multiple 1 -choiceprefix 0 -choicerestricted 0 -choices { @@ -7619,7 +7697,7 @@ tcl::namespace::eval textblock { } -help "Perform an action on the frame cache." } proc frame_cache {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache] set action [dict get $argd values action] variable frame_cache set all_values_dict [dict get $argd values] @@ -7664,7 +7742,7 @@ tcl::namespace::eval textblock { endindex -default "" -type indexexpression } proc frame_cache_display {args} { - set argd [punk::args::get_by_id ::textblock::frame_cache_display $args] + set argd [punk::args::parse $args withid ::textblock::frame_cache_display] variable frame_cache lassign [dict values [dict get $argd values]] startidx endidx set limit "" @@ -7769,75 +7847,93 @@ tcl::namespace::eval textblock { # ${[textblock::frame_samples]} #todo punk::args alias for centre center etc? - punk::args::define { - @dynamic - @id -id ::textblock::frame - @cmd -name "textblock::frame"\ - -help "Frame a block of text with a border." - -checkargs -default 1 -type boolean\ - -help "If true do extra argument checks and - provide more comprehensive error info. - As the argument parser loads around 16 default frame - samples dynamically, this can add add up as each may - take 10s of microseconds. For many-framed tables - and other applications this can add up. - Set false for performance improvement." - -etabs -default 0\ - -help "expanding tabs - experimental/unimplemented." - -type -default light -choices {${[textblock::frametypes]}} -choicerestricted 0 -choicecolumns 8 -type dict\ - -choicelabels { - ${[textblock::frame_samples]} - }\ - -help "Type of border for frame." - -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. - passing an empty string will result in no box, but title/subtitle will still appear if supplied. - ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" - -boxmap -default {} -type dict - -joins -default {} -type list - -title -default "" -type string -regexprefail {\n}\ - -help "Frame title placed on topbar - no newlines. - May contain ANSI - no trailing reset required. - ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing - e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" - -titlealign -default "centre" -choices {left centre right} - -subtitle -default "" -type string -regexprefail {\n}\ - -help "Frame subtitle placed on bottombar - no newlines - May contain Ansi - no trailing reset required." - -subtitlealign -default "centre" -choices {left centre right} - -width -default "" -type int\ - -help "Width of resulting frame including borders. - If omitted or empty-string, the width will be determined automatically based on content." - -height -default "" -type int\ - -help "Height of resulting frame including borders." - -ansiborder -default "" -type ansistring\ - -help "Ansi escape sequence to set border attributes. - ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents - e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" - -ansibase -default "" -type ansistring\ - -help "Default ANSI attributes within frame." - -blockalign -default centre -choices {left right centre}\ - -help "Alignment of the content block within the frame." - -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background - extends within the content block inside the frame. - Has no effect if no ANSI in content." - -textalign -default left -choices {left right centre}\ - -help "Alignment of text within the content block. (centre unimplemented)" - -ellipsis -default 1 -type boolean\ - -help "Whether to show elipsis for truncated content and title/subtitle." - -usecache -default 1 -type boolean - -buildcache -default 1 -type boolean - -crm_mode -default 0 -type boolean\ - -help "Show ANSI control characters within frame contents. - (Control Representation Mode) - Frame width doesn't adapt and content may be truncated - so -width may need to be manually set to display more." + namespace eval argdoc { + punk::args::define { + @dynamic + @id -id ::textblock::frame + @cmd -name "textblock::frame"\ + -summary "Frame a block of content with a border."\ + -help\ + "This command allows content to be framed with various border styles. The content can include + other ANSI codes and unicode characters. Some predefined border types can be selected with + the -type option and the characters can be overridden either in part or in total by supplying + some or all entries in the -boxmap dictionary. + The ${$B}textblock::framedef${$N} command can be used to return a dictionary for a frame type. + Border elements can also be suppressed on chosen sides with -boxlimits. + ANSI colours can be applied to borders or as defaults for the content using -ansiborder and + -ansibase options. + The punk::ansi::a+ function (aliased as a+) can be used to apply ANSI styles. + e.g + frame -type block -ansiborder [a+ blue Red] -ansibase [a+ black Red] \"A\\nB\"" + -checkargs -default 1 -type boolean\ + -help "If true do extra argument checks and + provide more comprehensive error info. + As the argument parser loads around 16 default frame + samples dynamically, this can add add up as each may + take 10s of microseconds. For many-framed tables + and other applications this can add up. + Set false for performance improvement." + -etabs -default 0\ + -help "expanding tabs - experimental/unimplemented." + -type -default light\ + -type dict\ + -typesynopsis {${$I}choice${$NI}|<${$I}dict${$NI}>}\ + -choices {${[textblock::frametypes]}}\ + -choicerestricted 0 -choicecolumns 8\ + -choicelabels { + ${[textblock::frame_samples]} + }\ + -help "Type of border for frame." + -boxlimits -default {hl vl tlc blc trc brc} -type list -help "Limit the border box to listed elements. + passing an empty string will result in no box, but title/subtitle will still appear if supplied. + ${[textblock::EG]}e.g: -frame -boxlimits {} -title things [a+ red White]my\\ncontent${[textblock::RST]}" + -boxmap -default {} -type dict + -joins -default {} -type list + -title -default "" -type string -regexprefail {\n}\ + -help "Frame title placed on topbar - no newlines. + May contain ANSI - no trailing reset required. + ${[textblock::EG]}e.g 1: frame -title My[a+ green]Green[a]Thing + e.g 2: frame -title [a+ red underline]MyThing${[textblock::RST]}" + -titlealign -default "centre" -choices {left centre right} + -subtitle -default "" -type string -regexprefail {\n}\ + -help "Frame subtitle placed on bottombar - no newlines + May contain Ansi - no trailing reset required." + -subtitlealign -default "centre" -choices {left centre right} + -width -default "" -type int\ + -help "Width of resulting frame including borders. + If omitted or empty-string, the width will be determined automatically based on content." + -height -default "" -type int\ + -help "Height of resulting frame including borders." + -ansiborder -default "" -type ansistring\ + -help "Ansi escape sequence to set border attributes. + ${[textblock::EG]}e.g 1: frame -ansiborder [a+ web-red] contents + e.g 2: frame -ansiborder \"\\x1b\\\[31m\" contents${[textblock::RST]}" + -ansibase -default "" -type ansistring\ + -help "Default ANSI attributes within frame." + -blockalign -default centre -choices {left right centre}\ + -help "Alignment of the content block within the frame." + -pad -default 1 -type boolean -help "Whether to pad within the ANSI so content background + extends within the content block inside the frame. + Has no effect if no ANSI in content." + -textalign -default left -choices {left right centre}\ + -help "Alignment of text within the content block. (centre unimplemented)" + -ellipsis -default 1 -type boolean\ + -help "Whether to show elipsis for truncated content and title/subtitle." + -usecache -default 1 -type boolean + -buildcache -default 1 -type boolean + -crm_mode -default 0 -type boolean\ + -help "Show ANSI control characters within frame contents. + (Control Representation Mode) + Frame width doesn't adapt and content may be truncated + so -width may need to be manually set to display more." - @values -min 0 -max 1 - contents -default "" -type string\ - -help "Frame contents - may be a block of text containing newlines and ANSI. - Text may be 'ragged' - ie unequal line-lengths. - No trailing ANSI reset required. - ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + @values -min 0 -max 1 + contents -default "" -type string\ + -help "Frame contents - may be a block of text containing newlines and ANSI. + Text may be 'ragged' - ie unequal line-lengths. + No trailing ANSI reset required. + ${[textblock::EG]}e.g: frame \"[a+ blue White] \\nMy blue foreground text on\\nwhite background\\n\"${[textblock::RST]}" + } } #options before content argument - which is allowed to be absent @@ -7886,7 +7982,8 @@ tcl::namespace::eval textblock { if {[lindex $args end-1] eq "--"} { set contents [lpop optlist end] set has_contents 1 - lpop optlist end ;#drop the end-of-opts flag + #lpop optlist end + ledit optlist end end;#drop the end-of-opts flag } else { set optlist $args set contents "" @@ -7928,7 +8025,6 @@ tcl::namespace::eval textblock { #never need to checkargs if only one argument supplied even if it looks like an option - as it will be treated as data to frame if {[llength $args] != 1 && (!$opts_ok || $check_args)} { #as frame is called a lot within table building - checking args can have a *big* impact on final performance. - #set argd [punk::args::get_by_id ::textblock::frame $args] set argd [punk::args::parse $args withid ::textblock::frame] set opts [dict get $argd opts] set contents [dict get $argd values contents] @@ -8530,7 +8626,8 @@ tcl::namespace::eval textblock { #puts "frame--->ansiwrap -rawansi [ansistring VIEW $opt_ansibase] $cache_inner" if {$opt_ansibase ne ""} { if {[punk::ansi::ta::detect $cache_inner]} { - set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + #set cache_inner [punk::ansi::ansiwrap -rawansi $opt_ansibase $cache_inner] + set cache_inner [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $cache_inner] } else { set cache_inner "$opt_ansibase$cache_inner\x1b\[0m" } @@ -8561,7 +8658,8 @@ tcl::namespace::eval textblock { #JMN test #assert - lhs, cache_inner, rhs non-ragged - so can use join_basic REVIEW #set cache_body [textblock::join -- {*}$cache_bodyparts] - set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + #set cache_body [textblock::join_basic -- {*}$cache_bodyparts] + set cache_body [textblock::join_basic_raw {*}$cache_bodyparts] append fscached $cache_body #append fs $body @@ -8622,7 +8720,8 @@ tcl::namespace::eval textblock { set contents_has_ansi [punk::ansi::ta::detect $contents] if {$opt_ansibase ne ""} { if {$contents_has_ansi} { - set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + #set contents [punk::ansi::ansiwrap -rawansi $opt_ansibase $contents] + set contents [punk::ansi::ansiwrap_raw $opt_ansibase "" "" $contents] } else { set contents "$opt_ansibase$contents\x1b\[0m" set contents_has_ansi 1 diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index 9809dc62..835fee21 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl @@ -181,16 +181,18 @@ set startdir [pwd] # ------------------------------------------------------------------------------------- set bootsupport_module_paths [list] set bootsupport_library_paths [list] +#we always create these lists in order of desired precedence. +# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add if {[file exists [file join $startdir src bootsupport]]} { + lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] ;#more version-specific modules slightly higher in precedence order lappend bootsupport_module_paths [file join $startdir src bootsupport modules] - lappend bootsupport_module_paths [file join $startdir src bootsupport modules_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] ;#more version-specific pkgs slightly higher in precedence order lappend bootsupport_library_paths [file join $startdir src bootsupport lib] - lappend bootsupport_library_paths [file join $startdir src bootsupport lib_tcl$::tclmajorv] } else { - lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_module_paths [file join $startdir bootsupport modules_tcl$::tclmajorv] - lappend bootsupport_library_paths [file join $startdir bootsupport lib] + lappend bootsupport_module_paths [file join $startdir bootsupport modules] lappend bootsupport_library_paths [file join $startdir bootsupport lib_tcl$::tclmajorv] + lappend bootsupport_library_paths [file join $startdir bootsupport lib] } set bootsupport_paths_exist 0 foreach p [list {*}$bootsupport_module_paths {*}$bootsupport_library_paths] { @@ -210,13 +212,13 @@ set sourcesupport_paths_exist 0 #(most?) Modules in src/modules etc should still be runnable directly in certain cases like this where we point to them. if {[file tail $startdir] eq "src"} { #todo - other src 'module' dirs.. - foreach p [list $startdir/modules $startdir/modules_tcl$::tclmajorv $startdir/vendormodules $startdir/vendormodules_tcl$::tclmajorv] { + foreach p [list $startdir/modules_tcl$::tclmajorv $startdir/modules $startdir/vendormodules_tcl$::tclmajorv $startdir/vendormodules] { if {[file exists $p]} { lappend sourcesupport_module_paths $p } } # -- -- -- - foreach p [list $startdir/lib $startdir/lib_tcl$::tclmajorv $startdir/vendorlib $startdir/vendorlib_tcl$::tclmajorv] { + foreach p [list $startdir/lib_tcl$::tclmajorv $startdir/lib $startdir/vendorlib_tcl$::tclmajorv $startdir/vendorlib] { if {[file exists $p]} { lappend sourcesupport_library_paths $p } @@ -273,16 +275,48 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package forget $pkg } } - #tcl::tm::add {*}$original_tm_list {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - #set ::auto_path [list {*}$original_auto_path {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] - tcl::tm::add {*}$bootsupport_module_paths {*}$sourcesupport_module_paths - set ::auto_path [list {*}$bootsupport_library_paths {*}$sourcesupport_library_paths] + #Deliberately omit original_tm_list and original_auto_path + tcl::tm::add {*}[lreverse $bootsupport_module_paths] {*}[lreverse $sourcesupport_module_paths] ;#tm::add works like LIFO. sourcesupport_module_paths end up earliest in resulting tm list. + set ::auto_path [list {*}$sourcesupport_library_paths {*}$bootsupport_library_paths] + } + puts "----> auto_path $::auto_path" + puts "----> tcl::tm::list [tcl::tm::list]" + + #maint: also in punk::repl package + #-------------------------------------------------------- + set libunks [list] + foreach tm_path [tcl::tm::list] { + set punkdir [file join $tm_path punk] + if {![file exists $punkdir]} {continue} + lappend libunks {*}[glob -nocomplain -dir $punkdir -type f libunknown-*.tm] + } + set libunknown "" + set libunknown_version_sofar "" + foreach lib $libunks { + #expecting to be of form libunknown-.tm + set vtail [lindex [split [file tail $lib] -] 1] + set thisver [file rootname $vtail] ;#file rootname x.y.z.tm + if {$libunknown_version_sofar eq ""} { + set libunknown_version_sofar $thisver + set libunknown $lib + } else { + if {[package vcompare $thisver $libunknown_version_sofar] == 1} { + set libunknown_version_sofar $thisver + set libunknown $lib + } + } } - puts "----> auto_path $::auto_path" - - + if {$libunknown ne ""} { + source $libunknown + if {[catch {punk::libunknown::init -caller main.tcl} errM]} { + puts "error initialising punk::libunknown\n$errM" + } + } + #-------------------------------------------------------- #package require Thread + puts "---->tcl_library [info library]" + puts "---->loaded [info loaded]" # - the full repl requires Threading and punk,shellfilter,shellrun to call and display properly. # tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list @@ -297,6 +331,8 @@ if {$bootsupport_paths_exist || $sourcesupport_paths_exist} { package require punk::lib package require punk::args package require punk::ansi + package require textblock + set package_paths_modified 1 @@ -1217,15 +1253,20 @@ if {$::punkboot::command eq "check"} { #don't exit yet - 2nd part of "check" below package path restore } # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - package path restore original module paths and auto_path entries to take effect in addition to bootsupport paths +# - Order such that bootsupport entries are always higher priority (if same version number - prefer bootsupport) # - This must be done between the two "check" command sections if {$package_paths_modified} { - set tm_list_now [tcl::tm::list] - foreach p $original_tm_list { - if {$p ni $tm_list_now} { + set tm_list_boot [tcl::tm::list] + tcl::tm::remove {*}$tm_list_boot + foreach p [lreverse $original_tm_list] { + if {$p ni $tm_list_boot} { tcl::tm::add $p } } + foreach p [lreverse $tm_list_boot] { + tcl::tm::add $p + } #set ::auto_path [list $bootsupport_lib {*}$original_auto_path] lappend ::auto_path {*}$original_auto_path } @@ -1333,11 +1374,13 @@ if {$::punkboot::command eq "info"} { if {$::punkboot::command eq "shell"} { + puts stderr ">>>>>> loaded:[info loaded]" package require punk package require punk::repl - puts stderr "punk boot shell not implemented - dropping into ordinary punk shell" - #todo - make procs vars etc from this file available? + puts stderr "punk boot shell not implemented - dropping into ordinary punk shell." + + repl::init repl::start stdin @@ -1504,7 +1547,7 @@ if {$::punkboot::command eq "bootsupport"} { proc modfile_sort {p1 p2} { lassign [split [file rootname $p1] -] _ v1 - lassign [split [file rootname $p1] -] _ v2 + lassign [split [file rootname $p2] -] _ v2 package vcompare $v1 $v2 } proc bootsupport_localupdate {projectroot} { @@ -1543,7 +1586,10 @@ if {$::punkboot::command eq "bootsupport"} { set module_subpath [string map [list :: /] [namespace qualifiers $modulematch]] set srclocation [file join $projectroot $relpath $module_subpath] #puts stdout "$relpath $modulematch $module_subpath $srclocation" - if {[string first - $modulematch]} { + #we must always glob using the dash - or we will match libraries that are suffixes of others + #bare lib.tm with no version is not valid. + if {[string first - $modulematch] != -1} { + #version or part thereof is specified. set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]*.tm] } else { set pkgmatches [glob -nocomplain -dir $srclocation -tail -type f [namespace tail $modulematch]-*.tm] @@ -1566,6 +1612,7 @@ if {$::punkboot::command eq "bootsupport"} { #review set copy_files $pkgmatches } + #if a file added manually to target dir - there will be no .punkcheck record - will be detected as changed foreach cfile $copy_files { set srcfile [file join $srclocation $cfile] set tgtfile [file join $targetroot $module_subpath $cfile] @@ -1574,6 +1621,8 @@ if {$::punkboot::command eq "bootsupport"} { $boot_event targetset_init INSTALL $tgtfile $boot_event targetset_addsource $srcfile #---------- + # + #puts "bootsuport target $tgtfile record size: [dict size [$boot_event targetset_last_complete]]" if {\ [llength [dict get [$boot_event targetset_source_changes] changed]]\ || [llength [$boot_event get_targets_exist]] < [llength [$boot_event get_targets]]\ diff --git a/src/vfs/_config/punk_main.tcl b/src/vfs/_config/punk_main.tcl index c99c8a37..6ecce171 100644 --- a/src/vfs/_config/punk_main.tcl +++ b/src/vfs/_config/punk_main.tcl @@ -39,8 +39,12 @@ apply { args { #standard way to avoid symlinking issues - review! set normscript [file dirname [file normalize [file join [info script] __dummy__]]] + #The normalize is important as capitalisation must be retained (on all platforms) set normexe [file dirname [file normalize [file join [info nameofexecutable] __dummy__]]] + + puts stderr "STARKIT: [package provide starkit]" + set topdir [file dirname $normscript] set found_starkit_tcl 0 set possible_lib_vfs_folders [glob -nocomplain -dir [file join $topdir lib] -type d vfs*] @@ -53,10 +57,13 @@ apply { args { source $test_folder/pkgIndex.tcl } } - if {[set starkitv [lindex [package versions starkit] end]] ne ""} { + #package versions does not always return versions in increasing order! + if {[set starkitv [lindex [lsort -command {package vcompare} [package versions starkit]] end]] ne ""} { #run the ifneeded script for the latest found (assuming package versions ordering is correct) + puts "111 autopath: $::auto_path" eval [package ifneeded starkit $starkitv] set found_starkit_tcl 1 + puts "222 autopath: $::auto_path" } if {!$found_starkit_tcl} { #our internal 'quick' search for starkit failed. @@ -124,6 +131,8 @@ apply { args { if {[info exists ::tcl::kitpath] && $::tcl::kitpath ne ""} { set kp $::tcl::kitpath + set kp [file normalize $kp] ;#tcl::kitpath needs to be capitalised as per the actual path + #set existing_module_paths [string tolower [tcl::tm::list]] foreach p [list modules modules_tcl$tclmajorv] { #if {[string tolower [file join $kp $p]] ni $existing_module_paths} { @@ -280,6 +289,7 @@ apply { args { set external_tm_dirs [list] set lcase_internal_paths [string tolower $internal_paths] foreach tm $original_tm_list { + #review - do we know original tm list was properly normalised? (need capitalisation consistent for path keys) set tmlower [string tolower $tm] set is_internal 0 foreach okprefix $lcase_internal_paths { @@ -322,7 +332,8 @@ apply { args { lappend exe_module_folders $normexe_dir/modules lappend exe_module_folders $normexe_dir/modules_tcl$tclmajorv } - set nameexe_dir [file dirname [info nameofexecutable]] + set nameexe_dir [file dirname [file normalize [info nameofexecutable]]] ;#must be normalized for capitalisation consistency + #possible symlink (may resolve to same path as above - we check below to not add in twice) if {[file tail $nameexe_dir] eq "bin"} { lappend exe_module_folders [file dirname $nameexe_dir]/modules @@ -379,14 +390,14 @@ apply { args { } } else { #modules or modules_tclX subdir relative to cwd cannot be added if [pwd] has been added - set cwd_modules_folder [file normalize [file join [pwd] modules]] + set cwd_modules_folder [file join [pwd] modules] ;#pwd is already normalized to appropriate capitalisation if {[file isdirectory $cwd_modules_folder]} { if {[string tolower $cwd_modules_folder] ni [string tolower $external_tm_dirs]} { #prepend set external_tm_dirs [linsert $external_tm_dirs 0 $cwd_modules_folder] } } - set cwd_modules_folder [file normalize [file join [pwd] modules_tcl$tclmajorv]] + set cwd_modules_folder [file join [pwd] modules_tcl$tclmajorv] if {[file isdirectory $cwd_modules_folder]} { if {[string tolower $cwd_modules_folder] ni [string tolower $external_tm_dirs]} { #prepend @@ -693,7 +704,7 @@ apply { args { puts stderr "main.tcl tcl::tm::list:[tcl::tm::list]" } - if {$has_zipfs_attached} { + if {1 || $has_zipfs_attached} { #load libunknown without triggering the existing package unknown #maint: also in punk::repl package diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.0.tm deleted file mode 100644 index bee3ba09..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.0.tm +++ /dev/null @@ -1,5317 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.0 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - return -code error -errorcode {TCL WRONGARGS PUNK} $result - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define" - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance - #todo - configurable per interp/namespace - -errorstyle -type string -default enhanced -choices {enhanced standard minimal} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - set opts [dict merge $opts $defaultopts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS} {msg opts} { - #trap punk::args argument validation/parsing errors and decide here - #whether to display basic error - or full usage if configured. - puts stderr "PUNKARGS: $msg\n$opts" - return - } trap {} {msg opts} { - #review - #puts stderr "$msg\n$opts" - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $opts -errorcode] [dict get $opts -errorinfo] - return - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for [Get_caller]. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for [Get_caller]. Unexpected option \"$a\": No options defined while @opts -any 0" - } - arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for [Get_caller] requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for [Get_caller] requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for [Get_caller] didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - arg_error "Option $argname for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname]] $msg - arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.0 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.1.tm deleted file mode 100644 index b38715ad..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.1.tm +++ /dev/null @@ -1,5465 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.1 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.1] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define" - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - if {$argspecs ne ""} { - append msg \n [punk::lib::showdict -roottype list [info errorstack] */*] - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.1 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.2.tm deleted file mode 100644 index 16142ce4..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.2.tm +++ /dev/null @@ -1,5465 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.2 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.2] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define" - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set customdict [lrange $ecode 3 end] - set argspecs [punk::lib::dict_getdef $customdict -argspecs ""] - set badarg [punk::lib::dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - if {$argspecs ne ""} { - append msg \n [punk::lib::showdict -roottype list [info errorstack] */*] - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.2 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.3.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.3.tm deleted file mode 100644 index 649f8f8d..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.3.tm +++ /dev/null @@ -1,5468 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.3 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.3] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name. - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - set included_args [punk::args::system::punklib_ldiff $globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -badarg -type string -help\ - "name of an argument to highlight" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table laout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -aserror -return -scheme} $k] - switch -- $fullk { - -badarg { - set badarg $v - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -return -aserror" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - - #display options first then values - foreach argumentset [list [list $leading_val_names_display $leading_val_names] [list $opt_names_display $opt_names] [list $trailing_val_names_display $trailing_val_names]] { - lassign $argumentset argnames_display argnames - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - dict lappend formattedchoices $groupname $cdisplay - } - } - } else { - set formattedchoices $choicegroups - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - arg_error "" [punk::args::get_spec $real_id] -scheme punk_info {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define" - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - if {$argspecs ne ""} { - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list [info errorstack] */*] - } - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived] - } - - #proc sample1 {p1 args} { - # #*** !doctools - # #[call [fun sample1] [arg p1] [opt {?option value...?}]] - # #[para]Description of sample1 - # return "ok" - #} - - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.3 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.4.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.4.tm deleted file mode 100644 index 7f170ff4..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.4.tm +++ /dev/null @@ -1,5745 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.4 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.4] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::get_dict is made near the beginning of the proc with a cacheable first argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::get_dict { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# } $args]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with * are optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args call above may be something like: -#[para] opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::get_dict { -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# } $args]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::get_dict { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } [list $category $another_leading_arg] -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages [list] ;#fully loaded - variable loaded_info [dict create] ;#time - variable scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - variable scanned_info [dict create] ;#time and idcount - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages [dict create] - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - variable id_cache_rawdef [tcl::dict::create] - variable id_cache_spec [tcl::dict::create] - - variable argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - - variable argdata_cache [tcl::dict::create] - - variable id_counter 0 - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - Returns a dictionary representing the argument specifications. - - The return result can generally be ignored, as the record is stored keyed on the - @id -id value from the supplied definition. - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level begginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing and help display. - directives include: - %B%@id%N% ?opt val...? - spec-options: -id - %B%@cmd%N% ?opt val...? - spec-options: -name -help - %B%@leaders%N% ?opt val...? - spec-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - spec-options: -any - %B%@values%N% ?opt val...? - spec-options: -min -max - (used for trailing args that come after switches/opts) - %B%@argdisplay%N% ?opt val...? - spec-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - spec-options: -name -url - %B%@seealso%N% ?opt val...? - spec-options: -name -url (for footer - unimplemented) - - Some other spec-options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - dict get [resolve {*}$args] id - } - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - if {[dict exists $rawdef_cache $args]} { - set id [dict get $rawdef_cache $args -id] - set is_dynamic [dict get $rawdef_cache $args -dynamic] - } else { - set id [rawdef_id $args] - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - set optionspecs [uplevel 1 [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } else { - #we are always doing our first subst during the define.. shouldn't it be a separate resolve somehow? optionally so at least? - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel 1 [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} {return} - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned ( - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "punk::args::get_dict called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme} $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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(goodarg) [a+ green strike] - set CLR(goodchoice) [a+ reverse] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(goodarg) [a+ strike] - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } - } - if {$synopsis ne ""} { - set synopsis [string range $synopsis 0 end-1] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: $synopsis] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $c] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $c] - } else { - dict lappend formattedchoices $groupname $c - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set cdisplay $c - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $c] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $c] - } else { - dict lappend formattedchoices $groupname $c - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - set opts $opt_defaults - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $fullopt - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - set leaders_dict $LEADER_DEFAULTS - set num_leaders [llength $leaders] - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - set values_dict $val_defaults - set num_values [llength $values] - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "Option $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - dict get [get_spec $id] form_names - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set form * - if {[lindex $args 0] eq "-form"} { - set arglist [lrange $args 2 end] - set form [lindex $args 1] - } else { - set arglist $args - } - if {[llength $arglist] == 0} { - error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - } - set id [lindex $arglist 0] - set cmdargs [lrange $arglist 1 end] - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - set syn "" - #todo - -multiple etc - foreach f $form_names { - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - if {[dict get $arginfo -optional]} { - append syn " ?$argname?" - } else { - append syn " $argname" - } - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - append syn " ?$argname?..." - } else { - append syn " ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - append syn " ?$argname?" - } else { - append syn " ?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - append syn " $argname ?$argname...?" - } else { - append syn " $argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - append syn " $argname" - } else { - append syn " $argname <$tp>" - } - } - } - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - append syn " ?$argname?..." - } else { - append syn " ?$argname?" - } - } else { - if {[dict get $arginfo -multiple]} { - append syn " $argname ?$argname?..." - } else { - append syn " $argname" - } - } - } - append syn \n - } - return [string trim $syn \n] - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.4 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.5.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.5.tm deleted file mode 100644 index 95e8011c..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.5.tm +++ /dev/null @@ -1,6247 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.5 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.5] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - directive-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - directive-options: -any - %B%@values%N% ?opt val...? - directive-options: -min -max - (used for trailing args that come after switches/opts) - %B%@form%N% ?opt val...? - directive-options: -form -synopsis - (used for commands with multiple forms) - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@argdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (text to replace autogenerated arg info) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED {}\ - LEADER_NAMES {}\ - LEADER_MIN ""\ - LEADER_MAX ""\ - leaderspec_defaults $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - opt_default {}\ - opt_required {}\ - OPT_NAMES {}\ - opt_any {}\ - opt_solos {}\ - optspec_defaults $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - val_defaults {}\ - val_required {}\ - VAL_NAMES {}\ - val_min ""\ - val_max ""\ - valspec_defaults $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - argdisplay_info ""\ - ] - - #set argdata_dict [tcl::dict::create\ - # id $DEF_definition_id\ - # arg_info $arg_info\ - # arg_checks $arg_checks\ - # leader_defaults $leader_defaults\ - # leader_required $leader_required\ - # leader_names $leader_names\ - # leader_min $leader_min\ - # leader_max $leader_max\ - # leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - # leader_checks_defaults $leader_checks_defaults\ - # opt_defaults $opt_defaults\ - # opt_required $opt_required\ - # opt_names $opt_names\ - # opt_any $opt_any\ - # opt_solos $opt_solos\ - # optspec_defaults [dict get $F $firstformid optspec_defaults]\ - # opt_checks_defaults $opt_checks_defaults\ - # val_defaults $val_defaults\ - # val_required $val_required\ - # val_names $val_names\ - # val_min $val_min\ - # val_max $val_max\ - # valspec_defaults [dict get $F $firstformid valspec_defaults]\ - # val_checks_defaults $val_checks_defaults\ - # cmd_info $cmd_info\ - # doc_info $doc_info\ - # argdisplay_info $argdisplay_info\ - # id_info $id_info\ - # form_defs $F\ - #] - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - if {[lindex $args 0] eq "-dynamic"} { - set is_dynamic [lindex $args 1] - set textargs [lrange $args 2 end] - } - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - set opt_required [list] - set val_required [list] - - set opt_defaults [tcl::dict::create] - - set val_defaults [tcl::dict::create] - set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - set opt_any 0 - set val_min 0 - set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - foreach fid $record_form_ids { - #only use elements with matching form id? - #probably this feature mainly useful for _default anyway so that should be ok - #cooperative doc sets specified in same file could share via known form ids too - #todo argdisplay_info by fid - if {![dict size $argdisplay_info]} { - set argdisplay_info [dict get $copyfrom argdisplay_info] - } - #TODO - #create leaders opts vals depending on position of @default line? - #options on @default line to exclude/include sets??? - } - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - argdisplay { - #override the displayed argument table. - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - set argdisplay_info [dict merge $argdisplay_info $at_specs] - } - opts { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - dict set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid optspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -any - - -anyopts { - set opt_any $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple - - -prefix { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - dict set F $fid optspec_defaults $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid leaderspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid leaderspec_defaults $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid valspec_defaults] - - foreach {k v} $at_specs { - switch -- $k { - -form { - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - set val_min $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - set val_max $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid valspec_defaults $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @argdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - #lappend val_names $argname - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid optspec_defaults] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid valspec_defaults] - } else { - set spec_merged [dict get $F $fid leaderspec_defaults] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - lappend opt_solos $argname - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - lappend opt_required $argname - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - #lappend leader_required $argname - } else { - lappend val_required $argname - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #check ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - #confirm any valnames before last don't have -multiple key - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - error "bad key -multiple on argument spec for value '$valname' in command form:'$fid'. Only the last value argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata leaderspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata optspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata valspec_defaults] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - ARG_INFO [dict get $F $firstformid ARG_INFO]\ - ARG_CHECKS [dict get $F $firstformid ARG_CHECKS]\ - LEADER_DEFAULTS [dict get $F $firstformid LEADER_DEFAULTS]\ - LEADER_REQUIRED [dict get $F $firstformid LEADER_REQUIRED]\ - LEADER_NAMES [dict get $F $firstformid LEADER_NAMES]\ - LEADER_MIN [dict get $F $firstformid LEADER_MIN]\ - LEADER_MAX [dict get $F $firstformid LEADER_MAX]\ - leaderspec_defaults [dict get $F $firstformid leaderspec_defaults]\ - LEADER_CHECKS_DEFAULTS [dict get $F $firstformid LEADER_CHECKS_DEFAULTS]\ - opt_defaults $opt_defaults\ - opt_required $opt_required\ - OPT_NAMES [dict get $F $firstformid OPT_NAMES]\ - opt_any $opt_any\ - opt_solos $opt_solos\ - optspec_defaults [dict get $F $firstformid optspec_defaults]\ - OPT_CHECKS_DEFAULTS [dict get $F $firstformid OPT_CHECKS_DEFAULTS]\ - val_defaults $val_defaults\ - val_required $val_required\ - VAL_NAMES [dict get $F $firstformid VAL_NAMES]\ - val_min $val_min\ - val_max $val_max\ - valspec_defaults [dict get $F $firstformid valspec_defaults]\ - VAL_CHECKS_DEFAULTS [dict get $F $firstformid VAL_CHECKS_DEFAULTS]\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - argdisplay_info $argdisplay_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - FORM_INFO $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @argdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @argdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso @argdisplay} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict $defaults_key]" - dict set resultdict $directive [dict get $specdict $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso - @argdisplay { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key leaderspec_defaults} - @opts {set defaults_key optspec_defaults} - @values {set defaults_key valspec_defaults} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict leaderspec_defaults] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict leaderspec_defaults]" - dict set resultdict $type [dict get $specdict leaderspec_defaults] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} { - return - } - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict {*}[punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::get_dict {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} {} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme} $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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(goodarg) [a+ green strike] - set CLR(goodchoice) [a+ reverse] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(goodarg) [a+ strike] - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - if {"$argdisplay_header$argdisplay_body" eq ""} { - set is_custom_argdisplay 0 - } else { - set is_custom_argdisplay 1 - } - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict FORM_INFO] - dict for {fid finfo} $form_info { - set syn [Dict_getdef $finfo -synopsis ""] - if {$syn ne ""} { - append synopsis $syn \n - } else { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 80} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - append synopsis $form_synopsis \n - } - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $spec_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $spec_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $spec_dict OPT_NAMES] { - set arginfo [dict get $spec_dict ARG_INFO $c] - if {[dict get $arginfo -prefix]} { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - } else { - lappend opt_names_display $c - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $spec_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $spec_dict LEADER_NAMES] - set trailing_val_names [dict get $spec_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $spec_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $spec_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - } {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}} { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict {*}$definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - error "punk::args::parse - invalid call. < 3 args" - } - set parseargs [lindex $args 0] - set tailargs [lrange $args 1 end] - - set split [lsearch -exact $tailargs withid] - if {$split < 0} { - set split [lsearch -exact $tailargs withdef] - if {$split < 0} { - #punk::args::usage arg_error? - error "punk::args::parse - invalid call. keyword withid|withdef required" - } else { - set tailtype withdef - } - } else { - set tailtype withid - } - - set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - if {[llength $opts] % 2} { - error "punk::args::parse Even number of -flag val pairs required after arglist" - } - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - } - } - } - switch -- $tailtype { - withid { - if {[llength [lrange $tailargs $split+1 end]] != 1} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $tailargs $split+1] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist [lrange $tailargs $split+1 end] - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict {*}$deflist $parseargs] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {args} { - #see arg_error regarding considerations around unhappy-path performance - - #consider a better API - # - e.g punk::args::parse ?-flag val?... $arglist withid $id - # - e.g punk::args::parse ?-flag val?... $arglist withdef $def ?$def...? - #can the above be made completely unambiguous for arbitrary arglist?? - #e.g what if arglist = withdef and the first $def is also withdef ? - - - #*** !doctools - #[call [fun get_dict] [arg optionspecs] [arg rawargs]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def multiline-string optionspecs] - #[para] This a block of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #} $args - - set rawargs [lindex $args end] ;# args values to be parsed - #we take a definition list rather than argspecs - because the definition could be dynamic - set definition_args [lrange $args 0 end-1] - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$definition_args]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - # TODO - capitalise 'define' vars to make it a bit easier - # ----------------------------------------------- - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$val_min > 0 && [llength $rawargs] <= $val_min || [llength $rawargs] <= [llength $val_required]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN} { - break - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to opt_defaults here - - - set leaders [list] - set arglist {} - set post_values {} - #val_min, val_max - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$val_max == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $val_max - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest val_min is 0 - if {$remaining_args_including_this <= $val_min} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= val_min already covered above - if {$val_max != -1} { - #finite max number of vals - if {$remaining_args_including_this == $val_max} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ne $fullopt} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $rawargs 0 $i-1] - #set post_values [lrange $rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to val_min already done above - if {$val_max ne -1 && $remaining_args_including_this <= $val_max} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding val_max val_min - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$opt_any} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $optspec_defaults ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - #set opts $opt_defaults - #--------------------------------------- - set ordered_opts [dict create] - foreach o $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $opt_defaults $o]} { - dict set ordered_opts $o [dict get $opt_defaults $o] - } - } - #add in possible '-any true' opts after the defined opts - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - #---------------------------------------- - #set leaders_dict $LEADER_DEFAULTS ;#wrong - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $leaderspec_defaults - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - set validx 0 - set in_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #!!! review - ## Don't set values_dict to val_defaults - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $val_defaults] - #------------------------------------------ - - foreach valname $VAL_NAMES val $values { - if {$validx+1 > $num_values} { - break - } - if {$valname ne ""} { - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $val_defaults $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $val] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $val - } - set in_multiple $valname - } else { - tcl::dict::set values_dict $valname $val - } - lappend valnames_received $valname - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend values_dict $in_multiple $val - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $in_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $valspec_defaults - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - incr validx - incr positionalidx - } - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected at least $leadermin" $argspecs - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected exactly $leadermin" $argspecs - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - #arg_error "bad number of leading values for [Get_caller]. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" $argspecs - } - } - } - - if {$val_max == -1} { - #only check min - if {$num_values < $val_min} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected at least $val_min" $argspecs - } - } else { - if {$num_values < $val_min || $num_values > $val_max} { - if {$val_min == $val_max} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $val_min" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected exactly $val_min" $argspecs - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $val_min and $val_max inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $val_min max $val_max] -argspecs $argspecs]] $msg - #arg_error "bad number of trailing values for [Get_caller]. Got $num_values values. Expected between $val_min and $val_max inclusive" $argspecs - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $opt_required $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $val_required $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $opt_required $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $val_required $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $val_defaults $opt_defaults] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - set dname leaders_dict - } - option { - set dname opts - } - value { - set dname values_dict - } - } - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "Option $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "Option $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "Option '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] contains ansi - but -allow_ansi is false. Received: '$e'" - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'list'. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "Option '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -minsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "Option '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires list with -maxsize $checkval. Received len:[llength $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "Option $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "Option $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "Option '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires ansistring - but no ansi detected" $argspecs -badarg $argname - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "Option '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires globstring - but no glob characters detected" $argspecs -badarg $argname - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option '$argname' for [Get_caller] must be integer less than or equal to $high. Received: '$e'" $argspecs -badarg $argname - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "Option '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer greater than or equal to $low. Received: '$e'" $argspecs -badarg $argname - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e'" $argspecs -badarg $argname - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "Option '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be integer between $low and $high inclusive. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "Option $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'integer'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "Option $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #error "Option $argname for [Get_caller] requires type 'double'. Received: '$e'" - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "Option $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be between $low and $high. Received: '$e'" $argspecs -badarg $argname - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "Option $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'boolean'. Received: '$e_check'" $argspecs -badarg $argname - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "Option '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'dict' - must be key value pairs. Received: '$e_check'" $argspecs -badarg $argname - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "Option '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -minsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "Option '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires dict with -maxsize $checkval. Received dict size:[dict size $e_check] value:'$e_check'" $argspecs -badarg $argname - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - #set msg "Option $argname for [Get_caller] requires type '$type'. Received: '$e'" - #try trap? - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type]]] $result - #throw ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error $msg $argspecs -badarg $argname - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" $argspecs -badarg $argname - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing file" $argspecs -badarg $argname - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "Option $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type '$type'. Received: '$e' which is not an existing directory" $argspecs -badarg $argname - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "Option $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] requires type 'character'. Received: '$e' which is not a single character" $argspecs -badarg $argname - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - dict get [get_spec $id] form_names - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - set display "?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "?[lindex [dict get $arginfo -choices] 0]?" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display "?$argname?" - } else { - set display "?$I$argname$RST?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$I$argname$RST ?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "[lindex [dict get $arginfo -choices] 0]" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display "$I$argname$RST" - } - } - } - append syn " $display" - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::get_dict { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - } $args] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.5 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.6.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.6.tm deleted file mode 100644 index c3bf04b8..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.6.tm +++ /dev/null @@ -1,6400 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.6 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.6] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - directive-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - directive-options: -any - %B%@values%N% ?opt val...? - directive-options: -min -max - (used for trailing args that come after switches/opts) - %B%@form%N% ?opt val...? - directive-options: -form -synopsis - (used for commands with multiple forms) - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VALSPEC_DEFAULTS $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache $optionspecs]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache $optionspecs] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple - - -prefix { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -form -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - literal { - #value is the name of the argument - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -type literal - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache $optionspecs $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS]" - dict set resultdict $type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set speclist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$speclist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set rdef [raw_def $id] - if {$rdef eq ""} { - return - } - return [resolve {*}$rdef] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - #basic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - array set CLR {} - set CLR(errormsg) [a+ brightred] - set CLR(title) "" - set CLR(check) [a+ brightgreen] - set CLR(solo) [a+ brightcyan] - set CLR(choiceprefix) [a+ underline] - set CLR(badarg) [a+ brightred] - set CLR(goodarg) [a+ green strike] - set CLR(goodchoice) [a+ reverse] - set CLR(linebase_header) [a+ white] - set CLR(cmdname) [a+ brightwhite] - set CLR(groupname) [a+ bold] - set CLR(ansiborder) [a+ bold] - set CLR(ansibase_header) [a+ bold] - set CLR(ansibase_body) [a+ white] - - switch -- $scheme { - nocolour { - set CLR(errormsg) [a+ bold] - set CLR(title) [a+ bold] - set CLR(check) "" - set CLR(solo) "" - set CLR(badarg) [a+ reverse] ;#? experiment - set CLR(goodarg) [a+ strike] - set CLR(cmdname) [a+ bold] - set CLR(linebase_header) "" - set CLR(linebase) "" - set CLR(ansibase_body) "" - } - info { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightyellow bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightcyan bold] - set CLR(ansibase_header) [a+ cyan] - set CLR(ansibase_body) [a+ white] - } - error { - set CLR(errormsg) [a+ brightred bold] - set CLR(title) [a+ brightcyan bold] - set CLR(check) [a+ brightgreen bold] - set CLR(choiceprefix) [a+ brightgreen bold] - set CLR(groupname) [a+ cyan bold] - set CLR(ansiborder) [a+ brightyellow bold] - set CLR(ansibase_header) [a+ yellow] - set CLR(ansibase_body) [a+ white] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - #if {"$argdisplay_header$argdisplay_body" eq ""} { - # set is_custom_argdisplay 0 - #} else { - # set is_custom_argdisplay 1 - #} - - #temp - TODO - set argdisplay_header "" - set argdisplay_body "" - set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $form_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $form_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $c] - if {[dict get $arginfo -prefix]} { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - } else { - lappend opt_names_display $c - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - punk::args::parse $args withid ::punk::args::parse - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" - } - } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - set VAL_MIN 0 - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$VAL_MIN > 0 && [llength $rawargs] <= $VAL_MIN || [llength $rawargs] <= [llength $VAL_REQUIRED]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$VAL_MIN ne ""} { - if {[llength $rawargs] > $VAL_MIN} { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - #JJJJ - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - set valmax -1 - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ne $fullopt} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $rawargs 0 $i-1] - #set post_values [lrange $rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$OPT_ANY} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - #--------------------------------------- - set ordered_opts [dict create] - foreach o $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $o]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $o] - } - } - #add in possible '-any true' opts after the defined opts - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set ldridx 0 - set in_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - #---------------------------------------- - #set leaders_dict $LEADER_DEFAULTS ;#wrong - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - foreach leadername $LEADER_NAMES ldr $leaders { - if {$ldridx+1 > $num_leaders} { - break - } - if {$leadername ne ""} { - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $ldr - } - set in_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $ldr - } - lappend leadernames_received $leadername - } else { - if {$in_multiple ne ""} { - tcl::dict::lappend leaders_dict $in_multiple $ldr - lappend leadernames_received $in_multiple ;#deliberately allow dups! (as with opts and values) - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - incr ldridx - incr positionalidx - } - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #!!! review - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - incr nameidx - set val [lindex $values $validx] - if {$valname ne ""} { - if {[llength $valname] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname', but requires [llength $valname] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $strideval] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $strideval - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $strideval - } - lappend valnames_received $valname - } else { - if {$valname_multiple ne ""} { - if {[llength $valname_multiple] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname_multiple { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname_multiple', but requires [llength $valname_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname_multiple] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $strideval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - set positionalidx [expr {$start_position + $validx}] - } - #------------------------------------------ - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $OPT_REQUIRED $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - literal { - foreach e $vlist { - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $hight. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - set display "?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "?[lindex [dict get $arginfo -choices] 0]?" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display "?$argname?" - } else { - set display "?$I$argname$RST?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$I$argname$RST ?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "[lindex [dict get $arginfo -choices] 0]" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display "$I$argname$RST" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.6 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.7.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.7.tm deleted file mode 100644 index 1fbd03bb..00000000 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.1.7.tm +++ /dev/null @@ -1,6532 +0,0 @@ -# -*- tcl -*- -# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'pmix make' or bin/punkmake to update from -buildversion.txt -# module template: punkshell/src/decktemplates/vendor/punk/modules/template_module-0.0.2.tm -# -# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. -# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# (C) 2024 -# -# @@ Meta Begin -# Application punk::args 0.1.7 -# Meta platform tcl -# Meta license -# @@ Meta End - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# doctools header -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.1.7] -#[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 --}] -#[require punk::args] -#[keywords module proc args arguments parse] -#[description] -#[para]Utilities for parsing proc args - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section Overview] -#[para] There are many ways to parse arguments and many (too many?) packages to do it (see below for a discussion of packages and pure-tcl mechanisms). -#[para] overview of punk::args -#[subsection Concepts] -#[para]There are 2 main conventions for parsing a proc args list -#[list_begin enumerated] -#[enum] -#[para]leading option-value pairs and flags followed by a list of values (Tcl style) -#[enum] -#[para]leading list of values followed by option-value pairs and flags (Tk style) -#[list_end] -#[para]There are exceptions in both Tcl and Tk commands regarding this ordering -#[para]punk::args is focused on the 1st convention (Tcl style): parsing of the 'args' variable in leading option-value pairs (and/or solo flags) style -#[para]The proc can still contain some leading required values e.g [example "proc dostuff {arg1 arg2 args} {...}}"] -#[para]but having the core values elements at the end of args is arguably more generally useful - especially in cases where the number of trailing values is unknown and/or the proc is to be called in a functional 'pipeline' style. -#[para] -#[para]The basic principle is that a call to punk::args::parse is made near the beginning of the proc with a cacheable argument defining the parameters e.g -#[example { -# proc dofilestuff {args} { -# lassign [dict values [punk::args::parse $args withdef { -# @cmd -help "do some stuff with files e.g dofilestuff " -# @opts -type string -# #comment lines ok -# -directory -default "" -# -translation -default binary -# #setting -type none indicates a flag that doesn't take a value (solo flag) -# -nocomplain -type none -# @values -min 1 -max -1 -# }]] leaders opts values -# -# puts "translation is [dict get $opts -translation]" -# foreach f [dict values $values] { -# puts "doing stuff with file: $f" -# } -# } -#}] -#[para]The lines beginning with @ are usually optional in most cases and can be used to set defaults and some extra controls -#[para] - the above example would work just fine with only the - lines, but would allow zero filenames to be supplied as no -min value is set for @values -#[para]valid @ lines being with @cmd @leaders @opts @values -#[para]lines beginning with a dash define options - a name can optionally be given to each trailing positional argument. -#[para]If no names are defined for positional arguments, they will end up in the values key of the dict with numerical keys starting at zero. -#[para]e.g the result from the punk::args::parse call above may be something like: -#[para] leaders {} opts {-translation binary -directory "" -nocomplain 0} values {0 file1.txt 1 file2.txt 2 file3.txt} -#[para]Here is an example that requires the number of values supplied to be exactly 2 and names the positional arguments -#[para]It also demonstrates an inital argument 'category' that is outside of the scope for punk::args processing - allowing leading and trailing positional arguments -#[para]This could also be implemented entirely using args - and the @leaders category of arguments -#[example { -# proc dofilestuff {category args} { -# lassign [dict values [punk::args::parse $args withdef { -# @id -id ::dofilestuff -# -directory -default "" -# -translation -default binary -# -nocomplain -type none -# @values -min 2 -max 2 -# fileA -type existingfile 1 -# fileB -type existingfile 1 -# }]] leaders opts values -# puts "$category fileA: [dict get $values fileA]" -# puts "$category fileB: [dict get $values fileB]" -# } -#}] -#[para]By using standard tcl proc named arguments prior to args, and setting @values -min 0 -max 0 -#[para]a Tk-style ordering can be acheived, where punk::args is only handling the trailing flags and the values element of the returned dict can be ignored -#[para]This use of leading positional arguments means the type validation features can't be applied to them. It can be done manually as usual, -#[para] or an additional call could be made to punk::args e.g -#[example { -# punk::args::parse [list $category $another_leading_arg] withdef { -# category -choices {cat1 cat2 cat3} -# another_leading_arg -type boolean -# } -#}] - -#*** !doctools -#[subsection Notes] -#[para]For internal functions not requiring features such as solo flags, prefix matching, type checking etc - a well crafted switch statement will be the fastest pure-tcl solution. -#[para] -#When functions are called often and/or in inner loops, a switch based solution generally makes the most sense. -#For functions that are part of an API a package may be more suitable. -#[para]The following example shows a switch-based solution that is highly performant (sub microsecond for the no-args case) -#[example { -# proc test_switch {args} { -# set opts [dict create\\ -# -return "object"\\ -# -frametype "heavy"\\ -# -show_edge 1\\ -# -show_seps 0\\ -# -x a\\ -# -y b\\ -# -z c\\ -# -1 1\\ -# -2 2\\ -# -3 3\\ -# ] -# foreach {k v} $args { -# switch -- $k { -# -return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 { -# dict set opts $k $v -# } -# default { -# error "unrecognised option '$k'. Known options [dict keys $opts]" -# } -# } -# } -# return $opts -# } -#}] -#[para]Note that the switch statement uses literals so that the compiler produces a jump-table for best performance. -#[para] -# Attempting to build the switch branch using the values from dict keys $opts will stop the jump table being built. -# To create the faster switch statement without repeating the key names, the proc body would need to be built using string map. -#[para]use punk::lib::show_jump_tables to verify that a jump table exists. -#[para]Nearly as performant due to the c-coded tcl::prefix::match function built into Tcl is the following example - which also allows shortened option names if they are unambiguous -#[example { -# proc test_prefix {args} { -# set opts [dict create\ -# -return string\ -# -frametype \uFFEF\ -# -show_edge \uFFEF\ -# -show_seps \uFFEF\ -# -x a\ -# -y b\ -# -z c\ -# -1 1\ -# -2 2\ -# -3 3\ -# ] -# if {[llength $args]} { -# set knownflags [dict keys $opts] -# } -# foreach {k v} $args { -# dict set opts [tcl::prefix::match -message "test_prefix option $k" $knownflags $k] $v -# } -# return $opts -# } -#}] -#[para]There are many alternative args parsing packages a few of which are listed here. -#[list_begin enumerated] -#[enum]argp (pure tcl) -#[enum]parse_args (c implementation) -#[enum]argparse (pure tcl *) -#[enum]cmdline (pure tcl) -#[enum]opt (pure tcl) distributed with Tcl but considered deprecated -#[enum]The tcllib set of TEPAM modules (pure tcl) -#[para]TEPAM requires an alternative procedure declaration syntax instead of proc - but has support for Tk and documentation generation. -#[list_end] -#[para] (* c implementation planned/proposed) -#[para]punk::args was designed initially without specific reference to TEPAM - and to handle some edge cases in specific projects where TEPAM wasn't suitable. -#[para]In subsequent revisions of punk::args - some features were made to operate in a way that is similar to TEPAM - to avoid gratuitous differences where possible, but of course there are differences -#[para]and those used TEPAM or mixing TEPAM and punk::args should take care to assess the differences. -#[para]TEPAM is a mature solution and is widely available as it is included in tcllib. -#[para]Serious consideration should be given to using TEPAM or one of the other packages, if suitable for your project. -#[para]punk::args is relatively performant for a pure-tcl package solution - with the parsing of the argument specification block occuring only on the first run - after which a cached version of the spec is used. -#[para]punk::args is not limited to procs. It can be used in apply or coroutine situations for example. - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Requirements -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -#All ensemble commands are slower in a safe interp as they aren't compiled the same way -#https://core.tcl-lang.org/tcl/tktview/1095bf7f75 -#as this is needed in safe interps too, and the long forms tcl::dict::for tcl::string::map are no slower in a normal interp - we use the long form here. -#(As at 2024-06 There are many tcl8.6/8.7 interps in use which are affected by this and it's unknown when/whether it will be fixed) -#ensembles: array binary clock dict info namespace string -#possibly file too, although that is generally hidden/modified in a safe interp -#chan,encoding always seems to use invokeStk1 anyway - yet there are performance improvements using ::tcl::chan::names etc -#interestingly in tcl8.7 at least - tcl::namespace::eval $somens $somescript is slightly faster than namespace eval even in unsafe interp - -#*** !doctools -#[subsection dependencies] -#[para] packages used by punk::args -#[list_begin itemized] -package require Tcl 8.6- -#optional? punk::trie -#optional? punk::textblock -#*** !doctools -#[item] [package {Tcl 8.6-}] - -# #package require frobz -# #*** !doctools -# #[item] [package {frobz}] - -#*** !doctools -#[list_end] - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - -#*** !doctools -#[section API] - -tcl::namespace::eval punk::args::register { - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] cooperative namespace punk::args::register - #[para] punk::args aware packages may add their own namespace to the public list variable NAMESPACES before or after punk::args is loaded - #[para] The punk::args package will then test for a public list variable ::PUNKARGS containing argument definitions when it needs to. - #[list_begin definitions] - - #Although the actual punk::args::define calls are not too sluggish, there could be *many*. - #in a multi-interp environment, we want to be lazy about loading argdefs until they're actually required, - #especially since a fair proportion may be for documentation purposes rather than parsing args. - - # -- --- --- --- --- --- --- --- - #cooperative with packages that define some punk args but do so lazily - #These could be loaded prior to punk::args being loaded - so check existence of NAMESPACES var first - variable NAMESPACES ;#just declaring it with variable doesn't yet mean it 'exists' from 'info exists' perspective - if {![info exists ::punk::args::register::NAMESPACES]} { - set ::punk::args::register::NAMESPACES [list] - } - # -- --- --- --- --- --- --- --- - - variable loaded_packages - if {![info exists loaded_packages]} { - set loaded_packages [list] ;#fully loaded - } - variable loaded_info - if {![info exists loaded_info]} { - set loaded_info [dict create] ;#time - } - variable scanned_packages - if {![info exists scanned_packages]} { - set scanned_packages [list] ;#packages scanned for ids used to update namespace_docpackages - } - variable scanned_info ;#time and idcount - if {![info exists scanned_info]} { - set scanned_info [dict create] - } - #some packages, e.g punk::args::tclcore document other namespaces. - #when punk::args::update_definitions gets a query for a namespace - we need to load argdefs from registered sources - variable namespace_docpackages - if {![info exists namespace_docpackages]} { - set namespace_docpackages [dict create] - } - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::register ---}] -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Base namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args { - - - variable PUNKARGS ;#list of our own punk::args function argument definitions - loaded with punk::args::define at the end. - - tcl::namespace::export {[a-z]*} - variable rawdef_cache - if {![info exists rawdef_cache]} { - set rawdef_cache [tcl::dict::create] ;#key on rawdef list - return dict of {-id -dynamic 0|1} - } - variable id_cache_rawdef - if {![info exists id_cache_rawdef]} { - set id_cache_rawdef [tcl::dict::create] - } - variable id_cache_spec - if {![info exists id_cache_spec]} { - set id_cache_spec [tcl::dict::create] - } - - variable argdefcache_unresolved - if {![info exists argdefcache_unresolved]} { - set argdefcache_unresolved [tcl::dict::create] ;#key = original template list supplied to 'define', value = 2-element list: (tstr-parsed split of template) & (unresolved params) - } - - variable argdata_cache - if {![info exists argdata_cache]} { - set argdata_cache [tcl::dict::create] - } - - variable id_counter - if {![info exists id_counter]} { - set id_counter 0 - } - - #*** !doctools - #[subsection {Namespace punk::args}] - #[para] Core API functions for punk::args - #[list_begin definitions] - - #todo - some sort of punk::args::cherrypick operation to get spec from an existing set - #todo - doctools output from definition - - - - - #todo? -synonym/alias ? (applies to opts only not values) - #e.g -background -aliases {-bg} -default White - #review - how to make work with trie prefix - #e.g - # -corner -aliases {-corners} - # -centre -aliases {-center -middle} - #We mightn't want the prefix to be longer just because of an alias - #we should get -co -ce and -m from the above as abbreviations - - set map [list %B% \x1b\[1m %R% \x1b\[m %N% \x1b\[22m %I% \x1b\[3m %NI% \x1b\[23m ] - - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::define - #todo @preamble -help "move large block outside of table?" - @cmd -name punk::args::define -help\ - "Accepts a line-based definition of command arguments. - The definition can be supplied as a single text block or multiple as described - in the help information for 'text' below. - - Returns an id which is a key to the stored definition. - The id is taken from the supplied definition's @id -id line, or is an - automatically created id of the form 'autoid_'. - - At the time define is called - just the raw text arguments are stored for the id. - When the id is first used, for example with 'punk::args::parse $args withid $id', - the raw definition is parsed into a stored specifications dictionary. - - This specifications dictionary is structured for (optional) use within commands to - parse and validate the arguments - and is also used when retrieving definitions - (or parts thereof) for re-use. - - This can be used purely for documentation or called within a function to parse a mix - of leading values, switches/flags and trailing values. - - The overhead is favourably comparable with other argument processors - but none are - as fast as minimal code with a switch statement. For toplevel commands where a few - 10s of microseconds is immaterial, the validation and automated error formatting in - a table can be well worthwhile. For inner procs requiring utmost speed, the call can - be made only on the unhappy path when basic processing determines a mismatch - or it - can be left entirely as documentation for interactive use with: i ... - and for synopsis generation with: s ... - - The definition should usually contain an initial line of the form: @id -id ::somecmd - - Blank lines are ignored at the top level, ie if they are not part of another structure. - Similarly - lines at the top level beginning with the # character are ignored. - All other toplevel lines must consist of a leading word followed by paired arguments. - The arguments can be spread over multiple lines and contain lines of near-arbitrary - text if they are properly braced or double quoted and Tcl escaping for inner quotes - or unbalanced braces is maintained. - The line continuation character - (\\ at the end of the line) can be used to continue the set of arguments for - a leading word. - Leading words beginning with the @ character are directives controlling argument - parsing, defaults for subsequent arguments, and help display. - directives include: - %B%@id%N% ?opt val...? - directive-options: -id - %B%@cmd%N% ?opt val...? - directive-options: -name -help - %B%@leaders%N% ?opt val...? - directive-options: -min -max - (used for leading args that come before switches/opts) - %B%@opts%N% ?opt val...? - directive-options: -any - %B%@values%N% ?opt val...? - directive-options: -min -max - (used for trailing args that come after switches/opts) - %B%@form%N% ?opt val...? - directive-options: -form -synopsis - (used for commands with multiple forms) - The -synopsis value allows overriding the auto-calculated - synopsis. - %B%@formdisplay%N% ?opt val...? - directive-options: -header (text for header row of table) - -body (override autogenerated arg info for form) - %B%@doc%N% ?opt val...? - directive-options: -name -url - %B%@seealso%N% ?opt val...? - directive-options: -name -url (for footer - unimplemented) - - Some other options normally present on custom arguments are available - to use with the @leaders @opts @values directives to set defaults - for subsequent lines that represent your custom arguments. - These 3 directives should occur in exactly this order - but can be - repeated with custom argument lines interspersed. - - An @id line can only appear once and should be the first item. - For the commandline usage to be displayed either on parsing error - or using the i .. function - an @id with -id is needed. - - All directives can be omitted, in which case every line represents - a custom leader, value or option. - All will be leaders by default if no options defined. - If options are defined (by naming with leading dash, or explicitly - specifying @opts) then the definitions prior to the options will be - categorised as leaders, and those following the options will be - categorised as values. - - Custom arguments are defined by using any word at the start of a - line that doesn't begin with @ or - - (except that adding an additionl @ escapes this restriction so - that @@somearg becomes an argument named @somearg) - - custom leading args, switches/options (names starting with -) - and trailing values also take spec-options: - - -type - defaults to string. If no other restrictions - are specified, choosing string does the least validation. - recognised types: - none - (used for switches only. Indicates this is - a 'solo' flag ie accepts no value) - int|integer - list - indexexpression - dict - double - bool|boolean - char - file - directory - string - ansistring - globstring - (any of the types accepted by 'string is') - - These all perform some validation checks - - and more.. (todo - document here) - - -optional - (defaults to true for flags/switches false otherwise) - For non flag/switch arguments - all arguments with - -optional true must sit consecutively within their group. - ie all optional leader arguments must be together, and all - optional value arguments must be together. Furthermore, - specifying both optional leaders and optional values will - often lead to ambiguous parsing results. Currently, all - optional non-flg/switch arguments should be either at the - trailing end of leaders or the trailing end of values. - Further unambiguous arrangements of optional args may be - made in future - but are currently considered 'unsupported' - -default - -multiple (for leaders & values defines whether - subsequent received values are stored against the same - argument name - only applies to final leader OR final value) - (for options/flags this allows the opt-val pair or solo - flag to appear multiple times - not necessarily contiguously) - -choices {} - A list of allowable values for an argument. - The -default value doesn't have to be in the list. - If a -type is specified - it doesn't apply to choice members. - It will only be used for validation if the -choicerestricted - option is set to false. If all choices are specified in values - within the -choicegroups dict, it is not necessary to specify them - in the -choices list. It is effectively a simpler form of - specifying choices when no grouping is required. It is fine to - use both -choices and -choicegroups e.g specifying all in -choices - and then including only some that need grouping in -choicegroups. - -choicelabels {} - keys are the values/argument names from -choices (or equivalently - members of value entries from the -choicegroups dict) - The values in the choicelabels dict are text values, possibly - containing newlines, that are displayed below each choice. - This is commonly a very basic summary of the choice. In the - case of a subcommand it may be a usage synopsis for further - arguments. - -choicerestricted - Whether values not specified in -choices or -choicegroups are - allowed. Defaults to true. - -choiceprefix - This specifies whether unique prefixes are able to be used - instead of the complete string. This is calculated using - tcl::prefix::match - and will display in the autogenerated - usage output. Defaults to true. - -choiceprefixdenylist {} - These choices should match exactly a choice entry in one of - the settings -choices or -choicegroups. - These will still be used in prefix calculation - but the full - choice argument must be entered to select the choice. - -choicegroups {} - Generally this would be used instead of -choices to allow - usage display of choices grouped by some name (or the empty - string for 'ungrouped' items which appear first). - See for example the output if 'i zlib' where choices of the - next subcommand are grouped by the names compression,channel, - streaming and checksumming. The -choices list is equivalent - to a -choicegroups dict entry where the key (groupname) is - the empty string. Both may be specified, in which case the - final list of available choices will be a union of the listed - values in -choices and the values from each choice group. - Choice values specified in -choices are effectively ungrouped - unless overridden by placing them in a choicegroup. - -choicemultiple (default {1 1}) - is a pair representing min and max number of choices - that can be present in the value. - If is a single integer it is equivalent to a - specified with the same integer for both min and max. - Max of -1 represents no upper limit. - If allows more than one choice the value is a list - consisting of items in the choices made available through - entries in -choices/-choicegroups. - -minsize (type dependant) - -maxsize (type dependant) - -range (type dependant) - - - " - -dynamic -type boolean -default 0 -help\ - "If -dynamic is true, tstr interpolations of the form \$\{\$var\} - are re-evaluated on each call. - If the definition is being used not just as documentation, but is also - used within the function to parse args, e.g using punk::args::get_by_id, - then it should be noted that there is a slight performance penalty for the - dynamic case. - It is often not significant, perhaps depending on what vars/commands are - used but -dynamic true might be less desirable if the command is used in - inner loops in more performance-sensitive code. - " - @values -min 1 -max -1 - text -type string -multiple 1 -help\ - "Block(s) of text representing the argument definition for a command. - At least one must be supplied. If multiple, they are joined together with \\n. - Using multiple text arguments may be useful to mix curly-braced and double-quoted - strings to have finer control over interpolation when defining arguments. - (this can also be handy for sections that pull resolved definition lines - from existing definitions (by id) for re-use of argument specifications and help text) - - e.g the following definition passes 2 blocks as text arguments - definition { - @id -id ::myns::myfunc - @cmd -name myns::myfunc -help\\ - \"Description of command\" - - #The following option defines an option-value pair - -option1 -default blah -type string - #The following option defines a flag style option (solo) - -flag1 -default 0 -type none -help\\ - \"Info about flag1 - subsequent help lines auto-dedented by whitespace to left - of corresponding record start (in this case -flag1) - + first 4 spaces if they are all present. - This line has no extra indent relative to first line 'Info about flag1' - This line indented a further 6 chars\" - - @values -min 1 -max -1 - #Items that don't begin with * or - are value definitions - v1 -type integer -default 0 - thinglist -type string -multiple 1 - } \"*doc -name Manpage: -url \[myfunc_manpage_geturl myns::myfunc]\" - " - }]] - - proc New_command_form {name} { - #probably faster to inline a literal dict create in the proc than to use a namespace variable - set leaderspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -ensembleparameter 0\ - ] - set optspec_defaults [tcl::dict::create\ - -type string\ - -optional 1\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - -prefix 1\ - ] - set valspec_defaults [tcl::dict::create\ - -type string\ - -optional 0\ - -allow_ansi 1\ - -validate_ansistripped 0\ - -strip_ansi 0\ - -nocase 0\ - -choiceprefix 1\ - -choicerestricted 1\ - -choicemultiple {1 1}\ - -multiple 0\ - -regexprepass {}\ - -validationtransform {}\ - ] - - #form record can have running entries such as 'argspace' that aren't given to arg parser - #we could use {} for most default entry values - we just use {} as a hint for 'list' "" as a hint for string [tcl::dict::create] for dict - return [dict create\ - argspace "leaders"\ - ARG_INFO [tcl::dict::create]\ - ARG_CHECKS [tcl::dict::create]\ - LEADER_DEFAULTS [tcl::dict::create]\ - LEADER_REQUIRED [list]\ - LEADER_NAMES [list]\ - LEADER_MIN ""\ - LEADER_MAX ""\ - LEADERSPEC_DEFAULTS $leaderspec_defaults\ - LEADER_CHECKS_DEFAULTS {}\ - OPT_DEFAULTS [tcl::dict::create]\ - OPT_REQUIRED [list]\ - OPT_NAMES [list]\ - OPT_ANY 0\ - OPT_SOLOS {}\ - OPTSPEC_DEFAULTS $optspec_defaults\ - OPT_CHECKS_DEFAULTS {}\ - VAL_DEFAULTS [tcl::dict::create]\ - VAL_REQUIRED [list]\ - VAL_NAMES [list]\ - VAL_MIN ""\ - VAL_MAX ""\ - VALSPEC_DEFAULTS $valspec_defaults\ - VAL_CHECKS_DEFAULTS {}\ - FORMDISPLAY [tcl::dict::create]\ - ] - - } - - proc errorstyle {args} { - #set or query the running config -errorstyle - #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal - error todo - } - proc define {args} { - variable rawdef_cache - variable id_cache_rawdef - variable argdata_cache - if {[dict exists $rawdef_cache $args]} { - return [dict get [dict get $rawdef_cache $args] -id] - } else { - set id [rawdef_id $args] - if {[id_exists $id]} { - #we seem to be re-creating a previously defined id... - #clear any existing caches for this id - puts stderr "punk::args::define Redefinition of id:$id - clearing existing data" - - #dict unset argdata_cache $prevraw ;#silently does nothing if key not present - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - dict unset argdata_cache $k - } - } - dict for {k v} $rawdef_cache { - if {[dict get $v -id] eq $id} { - dict unset rawdef_cache $k - } - } - dict unset id_cache_rawdef $id - } - set is_dynamic [rawdef_is_dynamic $args] - set defspace [uplevel 1 {::namespace current}] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic -defspace $defspace] - dict set id_cache_rawdef $id $args - return $id - } - } - - proc idquery_info {id} { - variable id_cache_rawdef - variable rawdef_cache - if {[dict exists $id_cache_rawdef $id]} { - set sep [string repeat - 40] - set rawdef [dict get $id_cache_rawdef $id] - if {[dict exists $rawdef_cache $rawdef]} { - set idinfo [dict get $rawdef_cache $rawdef] - } else { - set idinfo "" - } - set result "raw definition:" - append result \n $sep - append result \n $rawdef - append result \n $sep - append result \n "id info:" - append result \n $idinfo - append result \n $sep - variable argdata_cache - #lsearch -stride not avail (or buggy) in some 8.6 interps - search manually for now (2025). todo - modernize some time after Tcl 9.0/9.1 more widespread.(2027?) - #check for and report if id is present multiple times - set argdata_records [list] - dict for {k v} $argdata_cache { - if {[dict get $v id] eq $id} { - if {$k eq $rawdef} { - lappend argdata_records [list 1 $k $v] - } else { - lappend argdata_records [list 0 $k $v] - } - } - } - append result \n "argdata cache:" - if {![llength $argdata_records]} { - append result \n "(not present)" - } else { - append result \n "present [llength $argdata_records] time(s)" - foreach r $argdata_records { - lassign $r match k v - if {$match} { - append result \n " - present with same rawdef key" - } else { - append result \n " - present with different rawdef key" - append result \n " [punk::lib::indent $k { }]" - } - } - if {[llength $argdata_records] > 1} { - append result \n "*more than one record was not expected - review*" - } - } - append result \n $sep - return $result - } - } - - proc define2 {args} { - dict get [resolve {*}$args] id - } - - proc resolve {args} { - variable rawdef_cache - variable id_cache_rawdef - set defspace "" - if {[dict exists $rawdef_cache $args]} { - set cinfo [dict get $rawdef_cache $args] - set id [dict get $cinfo -id] - set is_dynamic [dict get $cinfo -dynamic] - if {[dict exists $cinfo -defspace]} { - set defspace [dict get $cinfo -defspace] - } - } else { - #should we really be resolving something that hasn't been defined? - set id [rawdef_id $args] - puts stderr "Warning: punk::args::resolve called with undefined id:$id" - set is_dynamic [rawdef_is_dynamic $args] - dict set rawdef_cache $args [dict create -id $id -dynamic $is_dynamic] - dict set id_cache_rawdef $id $args - } - - - variable argdata_cache - variable argdefcache_unresolved - - - set cache_key $args - #ideally we would use a fast hash algorithm to produce a short key with low collision probability. - #something like md5 would be ok (this is non cryptographic) - but md5 uses open and isn't usable by default in a safe interp. (sha1 often faster on modern cpus) - #review - check if there is a built-into-tcl way to do this quickly - #for now we will just key using the whole string - #performance seems ok - memory usage probably not ideal - #quote from DKF 2021 - #> Dict keys can be any Tcl value; the string representation will be used as the actual value for computing the hash code. - #> It's probably a good idea to keep them comparatively short (kilobytes, not megabytes) for performance reasons, but it isn't critical. - #> There's no need to feel that the values (i.e., what they keys map to) are restricted at all. - #> You might hit overall memory limits if you compute the string representation of a very big dictionary; Tcl 8.* has limits there (in the low level API of its memory allocators). - #> If dealing with very large amounts of data, using a database is probably a good plan. - - set textargs $args - if {![llength $args]} { - punk::args::get_by_id ::punk::args::define {} - return - } - #if {[lindex $args 0] eq "-dynamic"} { - # set is_dynamic [lindex $args 1] - # set textargs [lrange $args 2 end] - #} - - #experimental - set LVL 2 - - if {!$is_dynamic} { - if {[tcl::dict::exists $argdata_cache $cache_key]} { - return [tcl::dict::get $argdata_cache $cache_key] - } - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - if {[string first \$\{ $optionspecs] > 0} { - if {$defspace ne ""} { - #normal/desired case - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } else { - #todo - deprecate/stop from happening? - puts stderr "punk::args::resolve calling tstr for id:$id with no known definition space (-defspace empty)" - set optionspecs [uplevel $LVL [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - } - } else { - - if {[tcl::dict::exists $argdefcache_unresolved $cache_key]} { - set pt_params [tcl::dict::get $argdefcache_unresolved $cache_key] - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - } else { - set normargs [list] - foreach a $textargs { - lappend normargs [tcl::string::map {\r\n \n} $a] - } - set optionspecs [join $normargs \n] - #dynamic - double substitution required. - #e.g - # set DYN_CHOICES {${[::somewhere::get_choice_list]}} - # set RED [punk::ansi::a+ bold red] - # set RST [punk::ansi::a] - # punk::args::define { - # -arg -choices {${$DYN_CHOICES}} -help "${$RED}important info${$RST}" - #} - if {$defspace ne ""} { - set optionspecs [namespace eval $defspace [list punk::args::lib::tstr -return string -eval 1 -allowcommands $optionspecs]] - } - #REVIEW - join also serves to unescape things such as \$\{$x\} \$\{[cmd]\} without subst'ing or evaling (?) - if {[string first \$\{ $optionspecs] > 0} { - set pt_params [punk::args::lib::tstr -return list -eval 0 $optionspecs] ;#-eval 0 - no need to uplevel - lassign $pt_params ptlist paramlist - set optionspecs "" - foreach pt $ptlist param $paramlist { - append optionspecs $pt [uplevel $LVL [list ::subst $param]] - } - tcl::dict::set argdefcache_unresolved $cache_key $pt_params - } - } - #argdata_cache should be limited in some fashion or will be a big memory leak??? - if {[tcl::dict::exists $argdata_cache [list $optionspecs]]} { - #resolved cache version exists - return [tcl::dict::get $argdata_cache [list $optionspecs]] - } - } - - - - #we need -choiceprefix and -choicerestricted defaults even though they often don't apply so we can look them up to display in Help if there are -choices - #default to 1 for convenience - - #checks with no default - #-minsize -maxsize -range - - - #default -allow_ansi to 1 and -validate_ansistripped to 0 and -strip_ansi 0 - it takes time to strip ansi - #todo - detect if anything in the spec uses -allow_ansi 0, -validate_ansistripped 1 or -strip_ansi 1 and set a flag indicating if punk::ansi::ta::detect should be run on the argslist - - #set opt_defaults [tcl::dict::create] - #set val_defaults [tcl::dict::create] - - #set opt_solos [list] - #first process dashed and non-dashed record names without regard to whether non-dashed are at the beginning or end - - set records [list] - set linebuild "" - - set linelist [split $optionspecs \n] - set lastindent "" - foreach ln $linelist { - if {[tcl::string::trim $ln] eq ""} {continue} - regexp {(\s*).*} $ln _all lastindent - break ;#break at first non-empty - } - #puts "indent1:[ansistring VIEW $lastindent]" - set in_record 0 - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - foreach rawline $linelist { - set recordsofar [tcl::string::cat $linebuild $rawline] - #ansi colours can stop info complete from working (contain square brackets) - #review - when exactly are ansi codes allowed/expected in record lines. - # - we might reasonably expect them in default values or choices or help strings - # - square brackets in ansi aren't and can't be escaped if they're to work as literals in the data. - # - eg set line "set x \"a[a+ red]red[a]\"" - # - 'info complete' will report 0, and subst would require -nocommand option or it will complain of missing close-bracket - if {$has_punkansi} { - set test_complete [punk::ansi::ansistrip $recordsofar] - } else { - #review - #we only need to strip enough to stop interference with 'info complete' - set test_complete [string map [list \x1b\[ ""] $recordsofar] - } - if {![tcl::info::complete $test_complete]} { - #append linebuild [string trimleft $rawline] \n - if {$in_record} { - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - #this allows alignment of multiline help strings to left margin whilst maintaining a visual indent in source form. - #Aligning subsequent lines with the record, or aligning 4 spaces in are equivalent. - #ie to indent lines further - whitespace should be added 4+ columns in from the record-line start position. - #(this leaves an oddity if indenting is only 1 2 or 3 spaces mixed with longer indents as we don't check for it.. REVIEW) - #(note string first "" $str is fast and returns -1) - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline \n - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline \n - } else { - append linebuild $rawline \n - } - } else { - set in_record 1 - regexp {(\s*).*} $rawline _all lastindent - #puts "indent: [ansistring VIEW -lf 1 $lastindent]" - #puts "indent from rawline:$rawline " - append linebuild $rawline \n - } - } else { - set in_record 0 - #trim only the whitespace corresponding to last record indent or lastindent + 4 spaces - not all whitespace on left - if {[tcl::string::first "$lastindent " $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length "$lastindent "] end] - append linebuild $trimmedline - } elseif {[tcl::string::first $lastindent $rawline] == 0} { - set trimmedline [tcl::string::range $rawline [tcl::string::length $lastindent] end] - append linebuild $trimmedline - } else { - append linebuild $rawline - } - lappend records $linebuild - set linebuild "" - } - } - set cmd_info {} - set package_info {} - set id_info {} ;#e.g -children ?? - set doc_info {} - #set argdisplay_info {} ;#optional override of autogenerated 'Arg Type Default Multi Help' table - set seealso_info {} - set keywords_info {} - ###set leader_min 0 - ###set leader_max 0 ;#default 0 but if leader_min is set switches to -1 for no limit - #set leader_max "" - #(common case of no leaders specified) - #set opt_any 0 - #set val_min 0 - #set val_max -1 ;#-1 for no limit - set DEF_definition_id $id - - #form_defs - set F [dict create _default [New_command_form _default]] - set form_ids_active [list _default] ;#list of form ids that subsequent directives and args are categorised under - - #set ARGSPACE [dict create] ;#keyed on form - #dict set ARGSPACE 0 "leaders" ;#leaders -> options -> values - - set refs [dict create] - set record_type "" - set record_number -1 ;# - foreach rec $records { - set trimrec [tcl::string::trim $rec] - switch -- [tcl::string::index $trimrec 0] { - "" - # {continue} - } - incr record_number - set record_values [lassign $trimrec firstword] ;#after first word, the remaining list elements up to the first newline that isn't inside a value, form a dict - if {[llength $record_values] % 2 != 0} { - #todo - avoid raising an error - store invalid defs keyed on id - error "punk::args::define - bad optionspecs line for record '$firstword' Remaining items on line must be in paired option-value format - received '$record_values' id:$DEF_definition_id" - } - # ---------------------------------------------------------- - # we (usually) don't use form ids for some directives such as @id and @doc - but we can check and set the form ids here for each record anyway. - #We deliberately don't set form_ids_active here *with one exception* for a rename of _default on first new name encountered in any record! - #(form_ids_active is otherwise set in the @form handling block) - - #consider the following 2 line entry which is potentially dynamically included via a tstr: - # @form -form {* newform} - # @form -form {newform} -synopsis "cmd help ?stuff?" - #If we omitted the first line - it would create a new form entry depending on whether it was the first record in the target location with a -form key or not. - #(because _default is usually 'taken over' by the first encountered form id) - #With both lines included - the first one matches all existing form ids, so newform is guaranteed to be a new record - #the first line will set all ids active - so the second line is necessary to bring it back to just newform - and have the -synopsis applied only to that record. - - if {[dict exists $record_values -form] && [llength [dict get $record_values -form]] > 0} { - set patterns [dict get $record_values -form] - set record_form_ids [list] - foreach p $patterns { - if {[regexp {[*?\[\]]} $p]} { - #isglob - only used for matching existing forms - lappend record_form_ids {*}[lsearch -all -inline -glob [dict keys $F] $p] - } else { - #don't test for existence - will define new form if necessary - lappend record_form_ids $p - } - } - #-form values could be globs that didn't match. record_form_ids could be empty.. - if {[llength $record_form_ids]} { - #only rename _default if it's the sole entry - if {[dict size $F] == 1 && [dict exists $F "_default"]} { - if {"_default" ni $record_form_ids} { - #only initial form exists - but we are mentioning new ones - #first rename the _default to first encountered new form id - #(just replace whole dict with new key - same data) - set F [dict create [lindex $record_form_ids 0] [dict get $F _default]] - #assert - _default must be only entry in form_ids_active - since there's only 1 record in $F - #we are only setting active because of the rename - @form is the way to change active forms list - set form_ids_active [lindex $record_form_ids 0] - } - } - foreach fid $record_form_ids { - if {![dict exists $F $fid]} { - if {$firstword eq "@form"} { - #only @form directly supplies keys - dict set F $fid [dict merge [New_command_form $fid] [dict remove $record_values -form]] - } else { - dict set F $fid [New_command_form $fid] - } - } else { - #update form with current record opts, except -form - if {$firstword eq "@form"} { dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] } - } - } - } - } else { - #missing or empty -form - set record_form_ids $form_ids_active - if {$firstword eq "@form"} { - foreach fid $form_ids_active { - dict set F $fid [dict merge [dict get $F $fid] [dict remove $record_values -form]] - } - } - } - # ---------------------------------------------------------- - - set firstchar [tcl::string::index $firstword 0] - set secondchar [tcl::string::index $firstword 1] - if {$firstchar eq "@" && $secondchar ne "@"} { - set record_type "directive" - set directive_name $firstword - set at_specs $record_values - - switch -- [tcl::string::range $directive_name 1 end] { - dynamic { - set is_dynamic 1 - } - id { - #disallow duplicate @id line ? - #review - nothing to stop multiple @id lines - or redefining as auto (which is ignored?) - - #id An id will be allocated if no id line present or the -id value is "auto" - - if {[dict exists $at_specs -id]} { - set thisid [dict get $at_specs -id] - if {$thisid ni [list $id auto]} { - error "punk::args::define @id mismatch existing: $id vs $thisid" - } - } - set id_info $at_specs - } - ref { - #a reference within the definition - #e.g see punk::args::tclcore ::after - #global reference dict - independent of forms - #ignore refs without an -id - #store all keys except -id - #complete overwrite if refid repeated later on - if {[dict exists $at_specs -id]} { - dict set refs [dict get $at_specs -id] [dict remove $at_specs -id] - } - } - default { - #copy from an identified set of *resolved*?? defaults (another argspec id) can be multiple - #(if we were to take from a definition - we would have to check and maybe change this def to -dynamic.. ?) - #perhaps we could allow -dynamic as a flag here - but IFF this define is already -dynamic (?) - #That is possibly too complicated and/or unnecessary? - #however.. as it stands we have define @dynamic making *immediate* resolutions .. is that really desirable? - - if {[dict exists $at_specs -id]} { - set copyfrom [get_spec [dict get $at_specs -id]] - #we don't copy the @id info from the source - #for now we only copy across if nothing set.. - #todo - bring across defaults for empty keys at targets? - #need to keep it simple enough to reason about behaviour easily.. - if {[dict size $copyfrom]} { - if {![dict size $cmd_info]} { - set cmd_info [dict get $copyfrom cmd_info] - } - if {![dict size $doc_info]} { - set doc_info [dict get $copyfrom doc_info] - } - - #foreach fid $record_form_ids { - # #only use elements with matching form id? - # #probably this feature mainly useful for _default anyway so that should be ok - # #cooperative doc sets specified in same file could share via known form ids too - # FORMDISPLAY has keys -header -body - # if {![dict size $F $fid $FORMDISPLAY]} { - # if {[dict exists $copyfrom FORMS $fid FORMDISPLAY]} { - # dict set F $fid FORMDISPLAY [dict get $copyfrom FORMS $fid FORMDISPLAY] - # } - # } - # #TODO - # #create leaders opts vals depending on position of @default line? - # #options on @default line to exclude/include sets??? - #} - } - } - } - form { - # arity system ? - #handle multiple parsing styles based on arities and keyword positions (and/or flags?) - #e.g see lseq manual with 3 different parsing styles. - #aim to produce a table/subtable for each - # @form -synopsis "start ?('..'|'to')? end ??'by'? step?"\ - # -arities { - # 2 - # {3 anykeys {1 .. 1 to}} - # {4 anykeys {3 by}} - # {5 anykeys {1 .. 1 to 3 by}} - # }\ - # -fallback 1 - # ... - # @parser -synopsis "start 'count' count ??'by'? step?"\ - # -arities { - # {3 anykeys {1 count}} - # } - # ... - # @form -synopsis "count ?'by' step?"\ - # -arities { - # 1 - # {3 anykeys {1 by}} - # } - # - # see also after manual - # @form -arities {1} - # @form -arities { - # 1 anykeys {0 info} - # } - #todo - - #can we generate a form synopsis if -synopsis not supplied? - - #form id can be list of ints|names?, or * - if {[dict exists $at_specs -form]} { - set idlist [dict get $at_specs -form] - if {$idlist eq "*"} { - #* only applies to form ids that exist at the time - set idlist [dict keys $F] - } - set form_ids_active $idlist - } - #new form keys already created if they were needed (done for all records that have -form ) - } - package { - set package_info [dict merge $package_info $at_specs] - } - cmd { - #allow arbitrary - review - set cmd_info [dict merge $cmd_info $at_specs] - } - doc { - set doc_info [dict merge $doc_info $at_specs] - } - formdisplay { - #override the displayed argument table for the form. - #(formdisplay keys -header -body) - #The opts,values etc are still parsed and used if they exist and if the definition is actually used in parsing - foreach fid $record_form_ids { - tcl::dict::set F $fid FORMDISPLAY [dict merge [tcl::dict::get $F $fid FORMDISPLAY] $at_specs] - } - } - opts { - foreach fid $record_form_ids { - if {[tcl::dict::get $F $fid argspace] eq "values"} { - error "punk::args::define - @opts declaration must come before @values (in command form: '$fid') - received '$record_values' id:$DEF_definition_id" - } - tcl::dict::set F $fid argspace "options" - set tmp_optspec_defaults [dict get $F $fid OPTSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -any - - -anyopts { - #set opt_any $v - tcl::dict::set F $fid OPT_ANY $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceinfo - -choiceprefix - -choiceprefixdenylist - -choicerestricted { - #review - only apply to certain types? - tcl::dict::set tmp_optspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels - -nocase { - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_optspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - index { - set v indexexpression - } - none - "" - - - any - ansistring - globstring - list { - - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_optspec_defaults -type $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple - - -prefix { - #allow overriding of defaults for options that occur later - tcl::dict::set tmp_optspec_defaults $k $v - } - default { - set known { -any -form -anyopts -minsize -maxsize -range -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple -prefix\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @opts line. Known keys: $known id:$DEF_definition_id" - } - } - } - tcl::dict::set F $fid OPTSPEC_DEFAULTS $tmp_optspec_defaults - } ;# end foreach record_form_ids - } - leaders { - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] in [list options values]} { - error "punk::args::define - @leaders declaration must come before all options and values (command form: '$fid') id:$DEF_definition_id" - } - set tmp_leaderspec_defaults [dict get $F $fid LEADERSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is 0. got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MIN $v - #if {$leader_max == 0} { - # set leader_max -1 - #} - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @leaders line is -1 (indicating unlimited). got $v id:$DEF_definition_id" - } - dict set F $fid LEADER_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -choiceinfo { - if {[llength $v] %2 != 0} { - error "punk::args::define - key '-choiceinfo' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - #-choicegroups? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_leaderspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_leaderspec_defaults $k $v - } - -ensembleparameter { - #review - tcl::dict::set tmp_leaderspec_defaults $k $v - #error "punk::args::define - -ensembleparameter not supported as a default for @leaders - only valid on actual leader arguments" - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @leaders line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid LEADERSPEC_DEFAULTS $tmp_leaderspec_defaults - - } ;#end foreach record_form_ids - - } - values { - foreach fid $record_form_ids { - dict set F $fid argspace "values" - - set tmp_valspec_defaults [dict get $F $fid VALSPEC_DEFAULTS] - - foreach {k v} $at_specs { - switch -- $k { - -form { - #review - handled above - } - -min - - -minvalues { - if {$v < 0} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is 0. got $v @id:$DEF_definition_id" - } - #set val_min $v - dict set F $fid VAL_MIN $v - } - -max - - -maxvalues { - if {$v < -1} { - error "punk::args::define - minimum acceptable value for key '$k' in @opts line is -1 (indicating unlimited). got $v @id:$DEF_definition_id" - } - #set val_max $v - dict set F $fid VAL_MAX $v - } - -minsize - -maxsize - -range - -choices - -choicemultiple - -choicecolumns - -choicelabels - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -nocase { - #review - only apply to certain types? - tcl::dict::set tmp_valspec_defaults $k $v - } - -choiceinfo - -choicegroups { - if {[llength $v] % 2 != 0} { - error "punk::args::define - key '$k' requires a dictionary value as an argument. got $v id:$DEF_definition_id" - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -nominsize - -nomaxsize - -norange - -nochoices - -nochoicelabels { - # -choicegroups ?? - if {$v} { - set k2 -[string range $k 3 end] ;#strip 'no' - tcl::dict::unset tmp_valspec_defaults $k2 - } - } - -type { - switch -- $v { - int - integer { - set v int - } - char - character { - set v char - } - bool - boolean { - set v bool - } - dict - dictionary { - set v dict - } - list { - - } - index { - set v indexexpression - } - default { - #todo - disallow unknown types unless prefixed with custom- - } - } - tcl::dict::set tmp_valspec_defaults $k $v - } - -optional - - -allow_ansi - - -validate_ansistripped - - -strip_ansi - - -regexprepass - - -regexprefail - - -regexprefailmsg - - -validationtransform - - -multiple { - tcl::dict::set tmp_valspec_defaults $k $v - } - default { - set known { -min -form -minvalues -max -maxvalues\ - -minsize -maxsize -range\ - -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase\ - -nominsize -nomaxsize -norange -nochoices -nochoicelabels\ - -type -optional -allow_ansi -validate_ansistripped -strip_ansi -multiple\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - } - error "punk::args::define - unrecognised key '$k' in @values line. Known keys: $known @id:$DEF_definition_id" - } - } - } - dict set F $fid VALSPEC_DEFAULTS $tmp_valspec_defaults - } - - } - seealso { - #todo! - #like @doc, except displays in footer, multiple - sub-table? - set seealso_info [dict merge $seealso_info $at_specs] - } - keywords { - #review - put as option on @cmd instead? @cmd -name xxx -keywords {blah etc hmm} ?? - set keywords_info [dict merge $keywords_info $at_specs] - } - default { - error "punk::args::define - unrecognised @ line in '$rec'. Expected @id @cmd @form... @leaders @opts @values @doc @formdisplay - use @@name if paramname needs to be @name @id:$DEF_definition_id" - } - } - #record_type directive - continue - } elseif {$firstchar eq "-"} { - set argname $firstword - set argdef_values $record_values - tcl::dict::set argdef_values -ARGTYPE option - - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - dict set F $fid argspace "options" - } elseif {[dict get $F $fid argspace] eq "values"} { - error "punk::args::define - invalid placement of line '$rec' - must come before @values (command form:'$fid') @id:$DEF_definition_id" - } - set record_type option - dict set F $fid OPT_NAMES [list {*}[dict get $F $fid OPT_NAMES] $argname] - #lappend opt_names $argname - } - - set is_opt 1 - } else { - set argname $firstword - if {$firstchar eq "@"} { - #allow basic @@ escaping for literal argname that begins with @ - set argname [tcl::string::range $argname 1 end] - } - - set argdef_values $record_values - foreach fid $record_form_ids { - if {[dict get $F $fid argspace] eq "leaders"} { - set record_type leader - tcl::dict::set argdef_values -ARGTYPE leader - #lappend leader_names $argname - set temp_leadernames [tcl::dict::get $F $fid LEADER_NAMES] - if {$argname ni $temp_leadernames} { - lappend temp_leadernames $argname - tcl::dict::set F $fid LEADER_NAMES $temp_leadernames - } else { - #This can happen if the definition has repeated values - error "punk::args::define - arg $argname already present as leader in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - - if {[dict get $F $fid LEADER_MAX] >= 0} { - if {[dict get $F $fid LEADER_MAX] < [llength $temp_leadernames]} { - puts stderr "punk::args::resolve warning arg $argname LEADER_MAX == [dict get $F $fid LEADER_MAX] but [llength $temp_leadernames] leader names found @id:$DEF_definition_id" - dict set F $fid LEADER_MAX [llength $temp_leadernames] - } - } - } else { - set record_type value - tcl::dict::set argdef_values -ARGTYPE value - set temp_valnames [tcl::dict::get $F $fid VAL_NAMES] - if {$argname ni $temp_valnames} { - lappend temp_valnames $argname - tcl::dict::set F $fid VAL_NAMES $temp_valnames - } else { - error "punk::args::define - arg $argname already present as value in '$rec' (command form:'$fid') @id:$DEF_definition_id" - } - #lappend val_names $argname - if {[dict get $F $fid VAL_MAX] >= 0} { - if {[dict get $F $fid VAL_MAX] < [llength $temp_valnames]} { - puts stderr "punk::args::resolve warning arg $argname VAL_MAX == [dict get $F $fid VAL_MAX] but [llength $temp_valnames] value names found @id:$DEF_definition_id" - dict set F $fid VAL_MAX [llength $temp_valnames] - } - } - } - } - - set is_opt 0 - } - - - #assert - we only get here if it is a value or flag specification line. - #assert argdef_values has been set to the value of record_values - - foreach fid $record_form_ids { - if {$is_opt} { - set spec_merged [dict get $F $fid OPTSPEC_DEFAULTS] - } else { - if {[dict get $F $fid argspace] eq "values"} { - set spec_merged [dict get $F $fid VALSPEC_DEFAULTS] - } else { - set spec_merged [dict get $F $fid LEADERSPEC_DEFAULTS] - } - } - - # -> argopt argval - foreach {spec specval} $argdef_values { - #literal-key switch - bytecompiled to jumpTable - switch -- $spec { - -form { - - } - -type { - #normalize here so we don't have to test during actual args parsing in main function - switch -- [tcl::string::tolower $specval] { - int - integer { - tcl::dict::set spec_merged -type int - } - bool - boolean { - tcl::dict::set spec_merged -type bool - } - char - character { - tcl::dict::set spec_merged -type char - } - dict - dictionary { - tcl::dict::set spec_merged -type dict - } - index - indexexpression { - tcl::dict::set spec_merged -type indexexpression - } - "" - none { - if {$is_opt} { - tcl::dict::set spec_merged -type none - if {[tcl::dict::exists $specval -optional] && [tcl::dict::get $specval -optional]} { - tcl::dict::set spec_merged -default 0 ;#-default 0 can still be overridden if -default appears after -type - we'll allow it. - } - } else { - #-solo only valid for flags - error "punk::args::define - invalid -type 'none' for positional argument positional argument '$argname' @id:$DEF_definition_id" - } - } - any - anything { - tcl::dict::set spec_merged -type any - } - ansi - ansistring { - tcl::dict::set spec_merged -type ansistring - } - any - string - globstring { - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - literal { - #value is the name of the argument - if {$is_opt} { - error "punk::args::define - invalid -type 'literal' for flag argument '$argname' @id:$DEF_definition_id" - } - tcl::dict::set spec_merged -type literal - } - default { - #allow custom unknown types through for now. Todo - require unknown types to begin with custom- REVIEW - tcl::dict::set spec_merged -type [tcl::string::tolower $specval] - } - } - } - -default - -solo - -range - - -choices - -choicegroups - -choicemultiple - -choicecolumns - -choiceprefix - -choiceprefixdenylist - -choicerestricted - -choicelabels - -choiceinfo - - -minsize - -maxsize - -nocase - -optional - -multiple - - -validate_ansistripped - -allow_ansi - -strip_ansi - -help - -ARGTYPE - - -regexprepass - -regexprefail - -regexprefailmsg - { - #inverses '-noxxx' (e.g -nochoices -nominsize etc) don't apply to specific args - only to @leaders @opts @values lines - #review -solo 1 vs -type none ? conflicting values? - tcl::dict::set spec_merged $spec $specval - } - -ensembleparameter { - #review - only leaders? - tcl::dict::set spec_merged $spec $specval - } - -prefix { - #for flags/options - tcl::dict::set spec_merged $spec $specval - } - -validationtransform { - #string is dict only 8.7/9+ - if {[llength $specval] % 2} { - error "punk::args::define - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary @id:$DEF_definition_id" - } - dict for {tk tv} $specval { - switch -- $tk { - -function - -type - -minsize - -maxsize - -range { - } - default { - set known_transform_keys [list -function -type -minsize -maxsize -range] ;#-choices etc? - error "punk::args::define - invalid subkey $tk for key '$spec' in specifications for argument '$argname' must be one of: $known_transform_keys @id:$DEF_definition_id" - } - } - } - - } - default { - if {[string match ref-* $spec]} { - #we don't validate keys taken from refs - this is a way to apply arbitrary keys to argdefs (classify as a feature-bug or an optimisation for now) - #ref-xxx will override an earlier -xxx in same line. That's fine. todo - document. - if {![tcl::dict::exists $refs $specval]} { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing @ref -id $specval (with $spec)" - } else { - set targetswitch [string range $spec 3 end] ;#capture - to form flag "-" - if {$targetswitch eq "-*"} { - set spec_merged [tcl::dict::merge $spec_merged [dict get $refs $specval]] ;#everything in @ref line except the -id - } else { - if {[tcl::dict::exists $refs $specval $targetswitch]} { - tcl::dict::set spec_merged $targetswitch [tcl::dict::get $refs $specval $targetswitch] - } else { - puts stderr "punk::args::define argument '$argname' attempt to reference non-existing subelement $targetswitch in @ref -id $specval (with $spec)" - } - } - } - } else { - set known_argopts [list -form -default -type -range -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choicerestricted\ - -nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help\ - -regexprepass -regexprefail -regexprefailmsg -validationtransform\ - -ensembleparameter\ - ] - error "punk::args::define - unrecognised key '$spec' in specifications for argument '$argname' Known option specification keys: $known_argopts @id:$DEF_definition_id" - } - } - } - } ;# end foreach {spec specval} argdef_values - - - if {$is_opt} { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - if {[tcl::dict::get $spec_merged -type] eq "none"} { - #JJJJ - dict set F $fid OPT_SOLOS [list {*}[dict get $F $fid OPT_SOLOS] $argname] - } - } else { - tcl::dict::set F $fid ARG_CHECKS $argname\ - [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - tcl::dict::set F $fid ARG_INFO $argname $spec_merged - #review existence of -default overriding -optional - if {![tcl::dict::get $spec_merged -optional] && ![tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - set temp_opt_required [dict get $F $fid OPT_REQUIRED] - lappend temp_opt_required $argname - dict set F $fid OPT_REQUIRED $temp_opt_required - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - set temp_leader_required [dict get $F $fid LEADER_REQUIRED] - lappend temp_leader_required $argname - dict set F $fid LEADER_REQUIRED $temp_leader_required - } else { - set temp_val_required [dict get $F $fid VAL_REQUIRED] - lappend temp_val_required $argname - dict set F $fid VAL_REQUIRED $temp_val_required - } - } - } - if {[tcl::dict::exists $spec_merged -default]} { - if {$is_opt} { - #tcl::dict::set opt_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - if {[dict get $F $fid argspace] eq "leaders"} { - tcl::dict::set F $fid LEADER_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } else { - #tcl::dict::set val_defaults $argname [tcl::dict::get $spec_merged -default] - tcl::dict::set F $fid VAL_DEFAULTS $argname [tcl::dict::get $spec_merged -default] - } - } - } - } ;# end foreach fid record_form_ids - - } ;# end foreach rec $records - - - #if {$DEF_definition_id eq "" || [tcl::string::tolower $DEF_definition_id] eq "auto"} { - # variable id_counter - # set DEF_definition_id "autoid_[incr id_counter]" - #} - - - #now cycle through ALL forms not just form_ids_active (record_form_ids) - dict for {fid formdata} $F { - # REVIEW - #no values specified - we can allow last leader to be multiple - foreach leadername [lrange [tcl::dict::get $F $fid LEADER_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $leadername -multiple]} { - error "bad key -multiple on argument spec for leader '$leadername' in command form:'$fid'. Only the last leader argument specification can be marked -multiple @id:$DEF_definition_id" - } - } - - #todo - disallow any -multiple == true entries if any leaders have -multiple == true? - #(creates parsing ambiguity) - #ambiguity could be resolved if at least one required option/flag eg -- - #ambiguities could theoretically also be resolved with required literals or choices - or even based on argument type - #(overcomplex? todo see if any core/tcllib commands work like that) - - #only allow a single entry within VAL_NAMES to have -multiple == true - #example of command with non-trailing -multiple == true is core command: 'file copy ?-force? ?--? source ?source?... targetDir - set val_multiples 0 - foreach valname [lrange [tcl::dict::get $F $fid VAL_NAMES] 0 end-1] { - if {[tcl::dict::get $F $fid ARG_INFO $valname -multiple]} { - if {$val_multiples > 0} { - error "bad setting -multiple true on argument spec for value '$valname' in command form:'$fid'. Only a single value argument specification can be marked with -multiple true @id:$DEF_definition_id" - } - incr val_multiples - } - } - - #todo - document that ambiguities in API are likely if both @leaders and @values used - #todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options) - - - dict set F $fid LEADER_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata LEADERSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid OPT_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata OPTSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - dict set F $fid VAL_CHECKS_DEFAULTS [tcl::dict::remove [dict get $formdata VALSPEC_DEFAULTS] -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi] ;#leave things like -range -minsize - } - - - - - #todo - precalculate a set of 'arity' entries for each form - #We want a structure for the arg parser to get easy access and make a fast decision on which form applies - #eg a classifier assistant might be total_arity ranges (where -1 is unlimited) ? - #1) after ms (1 1) - #2) after ms ?script...? (1 -1) (or is it 2 -1 ??) - should actually be #after ms script ?script...? - #3) after cancel id (2 2) - #4) after cancel script ?script...? (2 -1) - #5) after idle script ?script...? (1 -1) - #6) after info ?id? (1 2) - - #for arguments taking opts - total_arity generally unlimited (usually repeats allowed - they just override if not -multiple) - - #in the above case we have no unique total_arity - #we would also want to consider values when selecting - #e.g given the invalid command "after cancel" - # we should be selecting forms 3 & 4 rather than the exact arity match given by 1. - - - - set firstformid [lindex $F 0] ;#temporarily treat first form as special - as we can initially only parse single-form commands - #todo - for now: add -form flag to parse (get_dict etc) and require calling func to decide which form(s) to use - #even if we do eventually get automated multi-form parsing - it is useful to be able to restrict via -form flag, the parsing and doc generation to a specific form - #e.g commandline completion could show list of synopsis entries to select from - - set form_info [dict create] - dict for {fid fdict} $F { - dict set form_info $fid {} - dict for {optk optv} $fdict { - if {[string match -* $optk]} { - dict set form_info $fid $optk $optv - } - } - } - - set argdata_dict [tcl::dict::create\ - id $DEF_definition_id\ - cmd_info $cmd_info\ - doc_info $doc_info\ - package_info $package_info\ - seealso_info $seealso_info\ - id_info $id_info\ - FORMS $F\ - form_names [dict keys $F]\ - form_info $form_info\ - ] - - tcl::dict::set argdata_cache $cache_key $argdata_dict - if {$is_dynamic} { - #also cache resolved version - tcl::dict::set argdata_cache [list $optionspecs] $argdata_dict - } - - #tcl::dict::set id_cache_rawdef $DEF_definition_id $args - #puts "xxx:$result" - return $argdata_dict - } - - #return raw definition list as created with 'define' - # - possibly with unresolved dynamic parts - proc raw_def {id} { - variable id_cache_rawdef - set realid [real_id $id] - if {![dict exists $id_cache_rawdef $realid]} { - return "" - } - return [tcl::dict::get $id_cache_rawdef $realid] - } - - - namespace eval argdoc { - variable resolved_def_TYPE_CHOICES {* @id @package @cmd @ref @doc @formdisplay @seealso @leaders @opts @values leaders opts values} - variable resolved_def_TYPE_CHOICEGROUPS { - directives {@id @package @cmd @ref @doc @formdisplay @seealso} - argumenttypes {leaders opts values} - remaining_defaults {@leaders @opts @values} - } - - lappend PUNKARGS [list [string map [list %TYPECHOICES% $resolved_def_TYPE_CHOICES %TYPECHOICEGROUPS% $resolved_def_TYPE_CHOICEGROUPS] { - @id -id ::punk::args::resolved_def - @cmd -name punk::args::resolved_def -help\ - "Resolves or retrieves the previously resolved definition and - uses the 'spec' form to build a response in definition format. - - Pulling argument definition data from another function is a form - of tight coupling to the other function that should be done with - care. - - Note that the directives @leaders @opts @values may appear multiple - times in a source definition - applying defaults for arguments that - follow. When retrieving these - there is only a single result for - each that represents the defaults after all have been applied. - When retrieving -types * each of these will be positioned before - the arguments of that type - but this doesn't mean there was a single - leading directive for this argument type in the source definition. - Each argument has already had its complete specification recorded in - its own result. - - When manually specifying -types, the order @leaders then @opts then - @values must be maintained - but if they are placed before their - corresponding arguments, they will not affect the retrieved arguments - as these arguments are already fully spec'd. The defaults from the - source can be removed by adding @leaders, @opts @values to the - -antiglobs list, but again - this won't affect the existing arguments. - Each argument can have members of its spec overridden using the - -override dictionary. - " - @leaders -min 0 -max 0 - @opts - -return -default text -choices {text dict} - -form -default 0 -help\ - "Ordinal index or name of command form" - - #no restriction on number of types/repetitions? - -types -default * -choices {%TYPECHOICES%} -choicegroups {%TYPECHOICEGROUPS%} -choiceprefix 0 -choicemultiple {0 -1} - -antiglobs -default {} -type list -help\ - "Glob patterns for directive or argument/flags to - be suppressed" - -override -type dict -optional 1 -default "" -help\ - "dict of dicts. Key in outer dict is the name of a - directive or an argument. Inner dict is a map of - overrides/additions (- ...) for that line. - " - @values -min 1 -max -1 - id -type string -help\ - "identifer for a punk::args definition - This will usually be a fully-qualifed - path for a command name" - pattern -type string -optional 1 -default * -multiple 1 -help\ - "glob-style patterns for retrieving value or switch - definitions. - - If -type is * and pattern is * the entire definition including - directive lines will be returned in line form. - (directives are lines beginning with - @ e.g @id, @cmd etc) - - if -type is leaders,opts or values matches from that type - will be returned. - - if -type is another directive such as @id, @doc etc the - patterns are ignored. - - " - }]] - } - - - proc resolved_def {args} { - #not eating our own dogfood here as far as argument parsing. -id ::punk::args::resolved_def is for documentation/errors only. - set opts [dict create\ - -return text\ - -types {}\ - -form 0\ - -antiglobs {}\ - -override {}\ - ] - if {[llength $args] < 1} { - #must have at least id - punk::args::parse $args withid ::punk::args::resolved_def - return - } - set patterns [list] - - #a definition id must not begin with "-" ??? review - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {$a in {-type -types}} { - incr i - dict set opts -types [lindex $args $i] - } elseif {[string match -* $a]} { - incr i - dict set opts $a [lindex $args $i] - } else { - set id [lindex $args $i] - set patterns [lrange $args $i+1 end] - break - } - if {$i == [llength $args]-1} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - if {![llength $patterns]} { - set patterns [list *] - } - dict for {k v} $opts { - switch -- $k { - -return - -form - -types - -antiglobs - -override {} - default { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - } - set typelist [dict get $opts -types] - if {[llength $typelist] == 0} { - set typelist {*} - } - foreach type $typelist { - if {$type ni $::punk::args::argdoc::resolved_def_TYPE_CHOICES} { - punk::args::parse $args withid ::punk::args::resolved_def - return - } - } - - - variable id_cache_rawdef - set realid [real_id $id] - if {$realid eq ""} { - return - } - - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - set opt_form [dict get $opts -form] - if {[string is integer -strict $opt_form]} { - set formname [lindex [dict get $specdict form_names] $opt_form] - } else { - set formname $opt_form - } - set opt_override [dict get $opts -override] - set opt_return [dict get $opts -return] - - #set arg_info [dict get $specdict ARG_INFO] - set arg_info [dict get $specdict FORMS $formname ARG_INFO] - set argtypes [dict create leaders leader opts option values value] - - set opt_antiglobs [dict get $opts -antiglobs] - set directives [lsearch -all -inline -exact -not $::punk::args::argdoc::resolved_def_TYPE_CHOICES *] - set suppressed_directives [list] - set suppressed_args [list] - foreach ag $opt_antiglobs { - foreach d $directives { - if {[string match $ag $d]} { - lappend suppressed_directives $d - } - } - foreach argname [dict keys $arg_info] { - if {[string match $ag $argname]} { - lappend suppressed_args $argname - } - } - } - set suppressed_directives [lsort -unique $suppressed_directives] - set suppressed_args [lsort -unique $suppressed_args] - - set included_directives [punk::args::system::punklib_ldiff $directives $suppressed_directives] - - set globbed [list] - foreach pat $patterns { - set matches [dict keys $arg_info $pat] - lappend globbed {*}$matches - } - set globbed [lsort -unique $globbed] - #maintain order of original arg_info keys in globbed results - set ordered_globbed [list] - foreach a [dict keys $arg_info] { - if {$a ni $ordered_globbed && $a in $globbed} { - lappend ordered_globbed $a - } - } - set included_args [punk::args::system::punklib_ldiff $ordered_globbed $suppressed_args] - - set result "" - set resultdict [dict create] - foreach type $typelist { - switch -exact -- $type { - * { - if {"@id" in $included_directives} { - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - foreach directive {@package @cmd @doc @seealso} { - set dshort [string range $directive 1 end] - if {"$directive" in $included_directives} { - if {[dict exists $opt_override $directive]} { - append result \n "$directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict ${dshort}_info] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict ${dshort}_info]" - dict set resultdict $directive [dict get $specdict ${dshort}_info] - } - } - } - - #todo @formdisplay - - - #output ordered by leader, option, value - foreach pseudodirective {leaders opts values} tp {leader option value} { - set directive "@$pseudodirective" - switch -- $directive { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - - if {"$directive" in $included_directives} { - if {[dict exists $opt_override "$directive"]} { - append result \n "$directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]]" - dict set resultdict $directive [dict merge [dict get $specdict FORMS $formname $defaults_key] [dict get $opt_override $directive]] - } else { - append result \n "$directive [dict get $specdict FORMS $formname $defaults_key]" - dict set resultdict $directive [dict get $specdict FORMS $formname $defaults_key] - } - } - - if {$pseudodirective in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq $tp} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - - } - @id { - if {"@id" in $included_directives} { - #only a single id record can exist - if {[dict exists $opt_override @id]} { - append result \n "@id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]]" - dict set resultdict @id [dict merge [dict create -id [dict get $specdict id]] [dict get $opt_override @id]] - } else { - append result \n "@id -id [dict get $specdict id]" - dict set resultdict @id [list -id [dict get $specdict id]] - } - } - } - @package - @cmd - @doc - @seealso { - if {"$type" in $included_directives} { - set tp [string range $type 1 end] ;# @package -> package - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict ${tp}_info] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict ${tp}_info]" - dict set resultdict $type [dict get $specdict ${tp}_info] - } - } - #todo @formdisplay - } - @leaders - @opts - @values { - #these are the active defaults for further arguments - if {"$type" in $included_directives} { - switch -- $type { - @leaders {set defaults_key LEADERSPEC_DEFAULTS} - @opts {set defaults_key OPTSPEC_DEFAULTS} - @values {set defaults_key VALSPEC_DEFAULTS} - } - if {[dict exists $opt_override $type]} { - append result \n "$type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]]" - dict set resultdict $type [dict merge [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] [dict get $opt_override $type]] - } else { - append result \n "$type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS]" - dict set resultdict $type [dict get $specdict FORMS $formname LEADERSPEC_DEFAULTS] - } - } - } - leaders - opts - values { - #pseudo-directives - if {$type in $included_directives} { - foreach m $included_args { - set argspec [dict get $arg_info $m] - if {[dict get $argspec -ARGTYPE] eq [dict get $argtypes $type]} { - set argspec [dict remove $argspec -ARGTYPE] - if {[dict exists $opt_override $m]} { - append result \n "$m [dict merge $argspec [dict get $opt_override $m]]" - dict set resultdict $m [dict merge $argspec [dict get $opt_override $m]] - } else { - append result \n "$m $argspec" - dict set resultdict $m $argspec - } - } - } - } - } - default { - } - } - if {$opt_return eq "text"} { - return $result - } else { - return $resultdict - } - } - } - - proc resolved_def_values {id {patternlist *}} { - variable id_cache_rawdef - set realid [real_id $id] - if {$realid ne ""} { - set deflist [tcl::dict::get $id_cache_rawdef $realid] - set specdict [resolve {*}$deflist] - set arg_info [dict get $specdict ARG_INFO] - set valnames [dict get $specdict VAL_NAMES] - set result "" - if {$patternlist eq "*"} { - foreach v $valnames { - set def [dict get $arg_info $v] - set def [dict remove $def -ARGTYPE] - append result \n "$v $def" - } - return $result - } else { - foreach pat $patternlist { - set matches [dict keys $arg_info $pat] - set matches [lsearch -all -inline -glob $valnames $pat] - foreach m $matches { - set def [dict get $arg_info $m] - set def [dict remove $def -ARGTYPE] - append result \n "$m $def" - } - } - return $result - } - } - } - #proc resolved_def_leaders ?? - #proc resolved_def_opts ?? - - proc get_spec {id} { - set deflist [raw_def $id] - if {$deflist eq ""} { - return - } - return [resolve {*}$deflist] - #if {[id_exists $id]} { - # return [resolve {*}[raw_def $id]] - #} - } - proc is_dynamic {id} { - variable id_cache_rawdef - variable rawdef_cache - set deflist [raw_def $id] - if {[dict exists $rawdef_cache $deflist -dynamic]} { - return [dict get $rawdef_cache $deflist -dynamic] - } - return [rawdef_is_dynamic $deflist] - #@dynamic only has meaning as 1st element of a def in the deflist - } - - #@id must be within first 4 lines of a block - or assign auto - #review - @dynamic block where -id not explicitly set? - disallow? - proc rawdef_id {rawdef} { - set id "" - foreach d $rawdef { - foreach ln [lrange [split $d \n] 0 4] { - if {[regexp {\s*(\S+)(.*)} $ln _match firstword rest]} { - if {$firstword eq "@id"} { - if {[llength $rest] %2 == 0 && [dict exists $rest -id]} { - set id [dict get $rest -id] - break - } - } - } - } - if {$id ne ""} { - break - } - } - if {$id eq "" || [string tolower $id] eq "auto"} { - variable id_counter - set id "autoid_[incr id_counter]" - } - #puts "==>id: $id" - return $id - } - #test the rawdef for @dynamic directive - proc rawdef_is_dynamic {rawdef} { - #temporary - old way - set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]} ] - if {$flagged_dynamic} { - return true - } - foreach d $rawdef { - if {[regexp {\s*(\S+)} $d _match firstword]} { - if {$firstword eq "@dynamic"} { - return true - } - } - } - return false - } - - variable aliases - set aliases [dict create] - - lappend PUNKARGS [list { - @id -id ::punk::args::get_ids - @cmd -name punk::args::get_ids -help\ - "return list of ids for argument definitions" - @values -min 0 -max 1 - match -default * -help\ - "exact id or glob pattern for ids" - }] - proc get_ids {{match *}} { - variable id_cache_rawdef - variable aliases - return [list {*}[tcl::dict::keys $aliases $match] {*}[tcl::dict::keys $id_cache_rawdef $match]] - } - - #we don't automatically test for (autodef)$id - only direct ids and aliases - proc id_exists {id} { - variable aliases - if {[tcl::dict::exists $aliases $id]} { - return 1 - } - variable id_cache_rawdef - tcl::dict::exists $id_cache_rawdef $id - } - proc set_alias {alias id} { - variable aliases - dict set aliases $alias $id - } - proc unset_alias {alias} { - variable aliases - dict unset aliases $alias - } - proc get_alias {alias} { - variable aliases - if {[dict exists $aliases $alias]} { - return [tcl::dict::get $aliases $alias] - } - } - - proc real_id {id} { - variable id_cache_rawdef - variable aliases - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } else { - set check_updates [list [namespace qualifiers $id]] - #puts stderr "---->real_id '$id' update_definitions $check_updates" - if {![llength [update_definitions $check_updates]]} { - #nothing new loaded - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } else { - if {[tcl::dict::exists $aliases $id]} { - set id [tcl::dict::get $aliases $id] - } - if {[tcl::dict::exists $id_cache_rawdef $id]} { - return $id - } - if {[tcl::dict::exists $id_cache_rawdef (autodef)$id]} { - return (autodef)$id - } - return "" - } - } - } - - proc status {} { - upvar ::punk::args::register::NAMESPACES registered - upvar ::punk::args::register::loaded_packages loaded_packages - upvar ::punk::args::register::loaded_info loaded_info - upvar ::punk::args::register::scanned_packages scanned_packages - upvar ::punk::args::register::scanned_info scanned_info - set result "" - # [format %-${w0}s $idtail] - set widest [tcl::mathfunc::max {*}[lmap v [list {*}$registered "Registered"] {string length $v}]] - append result "[format %-${widest}s Registered] Scanned_ids Scantime_us Loaded_defs Loadtime_us" \n - set width_c2 [string length "Scanned_ids"] - set width_c3 [string length "Scantime_us"] - set width_c4 [string length "Loaded_defs"] - set width_c5 [string length "Loadtime_us"] - set count_unloaded 0 - set count_loaded 0 - foreach ns $registered { - if {$ns in $scanned_packages} { - set ids [dict get $scanned_info $ns idcount] - set scan_us [dict get $scanned_info $ns time] - } else { - set ids "" - set scan_us "" - } - if {$ns in $loaded_packages} { - incr count_loaded - set ldefs [dict get $loaded_info $ns defcount] - set load_us [dict get $loaded_info $ns time] - } else { - incr count_unloaded - set ldefs "" - set load_us "" - } - append result "[format %-${widest}s $ns] [format %${width_c2}s $ids] [format %${width_c3}s $scan_us] [format %${width_c4}s $ldefs] [format %${width_c5}s $load_us]" \n - } - append result "\nPackages - Registered: [llength $registered] Loaded: $count_loaded Unloaded: $count_unloaded" - return $result - } - - #scanned_packages (list) - #namespace_docpackages (dict) - proc update_definitions {{nslist *}} { - #puts "----> update_definitions '$nslist'" - if {[set gposn [lsearch $nslist {}]] >= 0} { - lset nslist $gposn :: - } - upvar ::punk::args::register::NAMESPACES registered ;#list - upvar ::punk::args::register::loaded_packages loaded_packages ;#list - upvar ::punk::args::register::loaded_info loaded_info ;#dict - upvar ::punk::args::register::scanned_packages scanned_packages ;#list - upvar ::punk::args::register::scanned_info scanned_info ;#dict - upvar ::punk::args::register::namespace_docpackages namespace_docpackages ;#dict - - - #puts stderr "-->update_definitions '$nslist'" - #needs to run quickly - especially when no package namespaces to be scanned for argdefs - #e.g - gets called for each subcommand of an ensemble (could be many) - # It needs to get called in each arginfo call as we don't know what namespace origins or aliases may be involved in resolving a command. - #we could possibly get away with not calling it for nested calls (such as with ensemble subcommands) but the code to avoid calls is probably more complex/slow than any gain avoiding the fast-path below. - # -- --- --- --- --- --- - # common-case fast-path - - if {[llength $loaded_packages] == [llength $registered]} { - #the only valid mechanism to add to 'loaded_packages' is with this function - so if lengths are equal, nothing to do. - #assert - if all are registered - then all have been scanned - return {} - } - # -- --- --- --- --- --- - - set unscanned [punklib_ldiff $registered $scanned_packages] - if {[llength $unscanned]} { - foreach pkgns $unscanned { - set idcount 0 - set ts_start [clock microseconds] - if {[info exists ${pkgns}::PUNKARGS]} { - set seen_documentedns [list] ;#seen per pkgns - foreach definitionlist [set ${pkgns}::PUNKARGS] { - #namespace eval $evalns [list punk::args::define {*}$definitionlist] - set id [rawdef_id $definitionlist] - if {[string match autoid_* $id]} { - puts stderr "update_definitions - unexpected autoid during scan of $pkgns - skipping" - puts stderr "definition:\n" - foreach d $definitionlist { - set out "" - foreach ln [split $d \n] { - append out " " $ln \n - } - puts $out - } - continue - } - #todo - detect duplicate ids (last will silently win.. should be reported somewhere) - incr idcount - set documentedns [namespace qualifiers $id] - if {$documentedns eq ""} {set documentedns ::} - if {$documentedns ni $seen_documentedns} { - #don't add own ns as a key in namespace_docpackages - if {$documentedns ne $pkgns} { - dict lappend namespace_docpackages $documentedns $pkgns - } - lappend seen_documentedns $documentedns - } - } - } - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - dict set scanned_info $pkgns [dict create time $diff idcount $idcount] - #we count it as scanned even if PUNKARGS didn't exist - #(registered the namespace, variable PUNKARGS may or may not be declared, but didn't set PUNKARGS) - lappend scanned_packages $pkgns - } - } - - - - if {"*" in $nslist} { - set needed [punklib_ldiff $registered $loaded_packages] - } else { - set needed [list] - foreach pkgns $nslist { - if {![string match ::* $pkgns]} { - puts stderr "warning: update_definitions received unqualified ns: $pkgns" - set pkgns ::$pkgns - } - if {$pkgns in $registered && $pkgns ni $loaded_packages} { - lappend needed $pkgns - } - #argdoc sub namespace is a standard place to put defs that match the namespace below - #(generally the PUNKARGS in a namespace should apply to own ns) - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - if {($pkgns in $registered || $docns in $registered) && $docns ni $needed && $docns ni $loaded_packages} { - lappend needed $docns - } - } - if {[dict exists $namespace_docpackages $pkgns]} { - #this namespace has other argdef sources - foreach docns [dict get $namespace_docpackages $pkgns] { - if {$docns ni $loaded_packages} { - lappend needed $docns - } - } - } - } - } - - - - set newloaded [list] - foreach pkgns $needed { - #puts stderr "update_definitions Loading: $pkgns" - set ts_start [clock microseconds] - set def_count 0 - if {![catch { - if {[info exists ${pkgns}::PUNKARGS]} { - set docns ${pkgns}::argdoc - if {[namespace exists $docns]} { - namespace eval ${pkgns}::argdoc { - set epath [namespace path] - set pkgns [namespace parent] - if {$pkgns ni $epath} { - namespace path [list {*}$epath $pkgns] ;#add to tail - } - - } - set evalns $docns - } else { - set evalns $pkgns - } - foreach definitionlist [set ${pkgns}::PUNKARGS] { - namespace eval $evalns [list punk::args::define {*}$definitionlist] - incr def_count - } - } - - #process list of 2-element lists - if {[info exists ${pkgns}::PUNKARGS_aliases]} { - foreach adef [set ${pkgns}::PUNKARGS_aliases] { - punk::args::set_alias {*}$adef - } - } - } errMsg]} { - set ts_end [clock microseconds] - set diff [expr {$ts_end - $ts_start}] - lappend loaded_packages $pkgns - lappend newloaded $pkgns - dict set loaded_info $pkgns [dict create time $diff defcount $def_count] - } else { - puts stderr "punk::args::update_definitions error - failed to load PUNKARGS definitions for $pkgns\nerr:$errMsg" - } - } - return $newloaded - } - - #for use within get_dict only - #This mechanism gets less-than-useful results for oo methods - #e.g {$obj} - proc Get_caller {} { - #set call_level -3 ;#for get_dict call - set call_level -4 - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - #puts "-->$cmdinfo" - #puts "-->[tcl::info::frame -3]" - while {[string last \n $cmdinfo] >= 1} { - #looks like a script - haven't gone up far enough? - #(e.g patternpunk oo system: >punk . poses -invalidoption) - incr call_level -1 - if {[catch { - set nextup [tcl::info::frame $call_level] - } ]} { - break - } - set cmdinfo [tcl::dict::get $nextup cmd] - set caller [regexp -inline {\S+} $cmdinfo] - if {[interp alias {} $caller] ne ""} { - #puts "found alias for caller $caller to [interp alias {} $caller]" - #see if we can go further - incr call_level -1 - if {[catch { - set cmdinfo [tcl::dict::get [tcl::info::frame $call_level] cmd] - } errM ]} { - puts "err: $errM" - break - } - } - } - set caller [regexp -inline {\S+} $cmdinfo] - if {$caller eq "namespace"} { - # review - message? - set cmdinfo "Get_caller (punk::args::get_dict?) called from namespace" - } - return $cmdinfo - } - - - # -------------------------------------- - #test of Get_caller - lappend PUNKARGS [list { - @id -id ::punk::args::test1 - @values -min 0 -max 0 - }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args - } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args - } - #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { - if {![llength $args]} { - puts "these test functions accept no arguments" - puts "Call with arg(s) to compare error output" - } - - if {[catch {test_get_dict {*}$args} errM]} { - puts $errM - } - puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { - puts $errM - } - return done - } - # -------------------------------------- - - set map "" - lappend PUNKARGS [list [string map $map { - @id -id ::punk::args::arg_error - @cmd -name punk::args::arg_error -help\ - "Generates a table (by default) of usage information for a command. - A trie system is used to create highlighted prefixes for command - switches and for subcommands or argument/switch values that accept - a defined set of choices. These prefixes match the mechanism used - to validate arguments (based on tcl::prefix::match). - - This function is called during the argument parsing process - (if the definition is not only being used for documentation) - It is also called by punk::args::usage which is in turn - called by the punk::ns introspection facilities which creates - on the fly definitions for some commands such as ensembles and - oo objects where a manually defined one isn't present. - " - @leaders -min 2 -max 2 - msg -type string -help\ - "Error message to display immediately prior to usage table. - May be empty string to just display usage. - " - spec_dict -type dict -help\ - "Dictionary of argument specifications. - This is the internal format parsed from - the textual definition. It contains the data - organised/optimised to allow the final arg - parser/validator to make decisions. - " - @opts - -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')" - -aserror -type boolean -help\ - "If true, the usage table is raised as an error message, - otherwise it is returned as a value." - -return -choices {string table tableobject} -choicelabels { - string "no table layout" - tableobject "table object cmd" - table "full table layout" - } - -scheme -default error -choices {nocolour info error} - -form -default 0 -help\ - "Ordinal index or name of command form" - }] ] - - if {[catch {package require punk::ansi}]} { - proc punk::args::a {args} {} - proc punk::args::a+ {args} {} - } else { - namespace eval ::punk::args { - namespace import ::punk::ansi::a ::punk::ansi::a+ - } - } - variable arg_error_CLR - array set arg_error_CLR {} - set arg_error_CLR(errormsg) [a+ brightred] - set arg_error_CLR(title) "" - set arg_error_CLR(check) [a+ brightgreen] - set arg_error_CLR(solo) [a+ brightcyan] - set arg_error_CLR(choiceprefix) [a+ underline] - set arg_error_CLR(badarg) [a+ brightred] - set arg_error_CLR(goodarg) [a+ green strike] - set arg_error_CLR(goodchoice) [a+ reverse] - set arg_error_CLR(linebase_header) [a+ white] - set arg_error_CLR(cmdname) [a+ brightwhite] - set arg_error_CLR(groupname) [a+ bold] - set arg_error_CLR(ansiborder) [a+ bold] - set arg_error_CLR(ansibase_header) [a+ bold] - set arg_error_CLR(ansibase_body) [a+ white] - variable arg_error_CLR_nocolour - array set arg_error_CLR_nocolour {} - set arg_error_CLR_nocolour(errormsg) [a+ bold] - set arg_error_CLR_nocolour(title) [a+ bold] - set arg_error_CLR_nocolour(check) "" - set arg_error_CLR_nocolour(solo) "" - set arg_error_CLR_nocolour(badarg) [a+ reverse] ;#? experiment - set arg_error_CLR_nocolour(goodarg) [a+ strike] - set arg_error_CLR_nocolour(cmdname) [a+ bold] - set arg_error_CLR_nocolour(linebase_header) "" - set arg_error_CLR_nocolour(linebase) "" - set arg_error_CLR_nocolour(ansibase_body) "" - variable arg_error_CLR_info - array set arg_error_CLR_info {} - set arg_error_CLR_info(errormsg) [a+ brightred bold] - set arg_error_CLR_info(title) [a+ brightyellow bold] - set arg_error_CLR_info(check) [a+ brightgreen bold] - set arg_error_CLR_info(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_info(groupname) [a+ cyan bold] - set arg_error_CLR_info(ansiborder) [a+ brightcyan bold] - set arg_error_CLR_info(ansibase_header) [a+ cyan] - set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error - array set arg_error_CLR_error {} - set arg_error_CLR_error(errormsg) [a+ brightred bold] - set arg_error_CLR_error(title) [a+ brightcyan bold] - set arg_error_CLR_error(check) [a+ brightgreen bold] - set arg_error_CLR_error(choiceprefix) [a+ brightgreen bold] - set arg_error_CLR_error(groupname) [a+ cyan bold] - set arg_error_CLR_error(ansiborder) [a+ brightyellow bold] - set arg_error_CLR_error(ansibase_header) [a+ yellow] - set arg_error_CLR_error(ansibase_body) [a+ white] - - - #bas ic recursion blocker - variable arg_error_isrunning 0 - proc arg_error {msg spec_dict args} { - #todo - test a configurable flag (in the CALLER) for whether to do a faster return on the unhappy path. - #accept an option here so that we can still use full output for usage requests. - #This may be desired for codebases where tests based on 'catch' are used on procs that parse with punk::args - #Development/experimentation may be done with full table-based error reporting - but for production release it - #may be desirable to reduce overhead on catches. - #consider per-namespace or namespace-tree configurability. - #In general - errors raised by this mechanism represent programming errors (or data sanity issues) rather than underlying errors due - #to resource availability etc - so the slower error generation time may not always be a problem. - #Contrary to that reasoning - validation options such as 'existingfile' are the sort of thing that might bubble up to a catch in calling - #code which has no use for the enhanced error info. - #The use of punk::args for arg parsing/validation is probably best suited for code close to an interactive user. - #consider also (erlang/elixer style?) message passing - to quickly hand off enhanced errors to another thread/system - #todo - #investigate options - e.g we return our errorcode {TCL WRONGARGS PUNK} quickly - and process the enhanced error - #asynchronously for later retrieval. (subcodes? e.g usage vs parameter validation fail) - - #todo - document unnamed leaders and unnamed values where -min and/or -max specified - #e.g punk::args::parse {} withdef {@leaders -min 1 -max 1} -x {@values -min 1 -max 2} - #only |?-x?|string|... is shown in the output table. - #should be something like: - # |arg | - # |?-x? | - # |arg | - # |?arg...?| - # Where/how to specify counts? - #also.. - # use multi column for displaying limits on -multiple true args/switches e.g -multimin x -multimax y? - # - - - #limit colours to standard 16 so that themes can apply to help output - variable arg_error_isrunning - if {$arg_error_isrunning} { - error "arg_error already running - error in arg_error?\n triggering errmsg: $msg" - } - - if {[llength $args] %2 != 0} { - error "error in arg_error processing - expected opt/val pairs after msg and spec_dict" - } - - set arg_error_isrunning 1 - - set badarg "" - set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] - set returntype table ;#table as string - set as_error 1 ;#usual case is to raise an error - set scheme error - set form 0 - dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -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 - } - -aserror { - if {![string is boolean -strict $v]} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -aserror. Received '$v' expected a boolean" - } - set as_error $v - } - -scheme { - set scheme $v - } - -return { - if {[tcl::prefix::match -error "" {string table tableobject} $v] eq ""} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -return. Received '$v' expected one of: string table tableobject" - } - set returntype $v - } - -form { - set form $v - } - default { - set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" - } - } - } - #todo - scheme - use config and iterm toml definitions etc - switch -- $scheme { - "" - -nocolor - -nocolour { - set scheme nocolour - } - info - error {} - default { - set scheme na - } - } - set formnames [dict get $spec_dict form_names] - if {[string is integer -strict $form]} { - if {$form < 0 || $form > [llength $formnames]-1} { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - set selected_forms [list [lindex $formnames $form]] - } else { - if {$form eq "*"} { - set selected_forms $formnames - } else { - if {$form in $formnames} { - set selected_forms [list $form] - } else { - set arg_error_isrunning 0 - error "arg_error invalid value for option -form. Received '$v' Allowed values 0-[expr {[llength $formnames]-1}] or one of '$formnames'" - } - } - } - - - #hack some basics for now. - #for coloured schemes - use bold as well as brightcolour in case colour off. - upvar ::punk::args::arg_error_CLR CLR - - switch -- $scheme { - nocolour { - variable arg_error_CLR_nocolour - array set CLR [array get arg_error_CLR_nocolour - } - info { - variable arg_error_CLR_info - array set CLR [array get arg_error_CLR_info] - } - error { - variable arg_error_CLR_error - array set CLR [array get arg_error_CLR_error] - } - na { - } - } - - - #set RST [a] - set RST "\x1b\[m" - set t "" ;#possible oo table object - may be tested for objectiness at the end so needs to exist. - - #REVIEW - risk of accidental indefinite recursion if functions used here also use punk::args::get_dict and there is an argument error - #e.g list_as_table - - # use basic colours here to support terminals without extended colours - #todo - add checks column (e.g -minsize -maxsize) - set errmsg $msg - if {![catch {package require textblock}]} { - set has_textblock 1 - } else { - set has_textblock 0 - #couldn't load textblock package - #just return the original errmsg without formatting - } - set use_table 0 - if {$has_textblock && $returntype in {table tableobject}} { - set use_table 1 - } - set errlines [list] ;#for non-textblock output - if {[catch { - if {$use_table} { - append errmsg \n - } else { - if {($returntype in {table tableobject}) && !$has_textblock} { - append errmsg \n "$CLR(errormsg)(layout package textblock is missing)$RST" \n - } else { - append errmsg \n - } - } - set cmdname [Dict_getdef $spec_dict cmd_info -name ""] - set cmdhelp [Dict_getdef $spec_dict cmd_info -help ""] - - set docname [Dict_getdef $spec_dict doc_info -name "Manual:"] - set docurl [Dict_getdef $spec_dict doc_info -url ""] - - #set argdisplay_header [Dict_getdef $spec_dict argdisplay_info -header ""] - #set argdisplay_body [Dict_getdef $spec_dict argdisplay_info -body ""] - #if {"$argdisplay_header$argdisplay_body" eq ""} { - # set is_custom_argdisplay 0 - #} else { - # set is_custom_argdisplay 1 - #} - - #temp - TODO - set argdisplay_header "" - set argdisplay_body "" - set is_custom_argdisplay 0 - - - set blank_header_col [list] - if {$cmdname ne ""} { - lappend blank_header_col "" - set cmdname_display $CLR(cmdname)$cmdname$RST - } else { - set cmdname_display "" - } - if {$cmdhelp ne ""} { - lappend blank_header_col "" - #set cmdhelp_display [a+ brightwhite]$cmdhelp[a] - set cmdhelp_display [textblock::ansibase_lines $cmdhelp $CLR(linebase_header)] - } else { - set cmdhelp_display "" - } - if {$docurl ne ""} { - lappend blank_header_col "" - set docurl_display [a+ white]$docurl$RST - } else { - set docurl_display "" - } - #synopsis - set synopsis "" - set form_info [dict get $spec_dict form_info] - dict for {fid finfo} $form_info { - set form_synopsis [Dict_getdef $finfo -synopsis ""] - if {$form_synopsis eq ""} { - #todo - set form_synopsis [punk::args::synopsis -form $fid [dict get $spec_dict id]] - if {[string length $form_synopsis] > 90} { - set form_synopsis [punk::args::synopsis -return summary -form $fid [dict get $spec_dict id]] - } - if {[string match (autodef)* $form_synopsis]} { - set form_synopsis [string range $form_synopsis 9 end] - } - } - if {$fid in $selected_forms} { - set form_synopsis [punk::ansi::a+ underline]$form_synopsis[punk::ansi::a+ nounderline] - } - append synopsis $form_synopsis \n - } - if {$synopsis ne ""} { - set synopsis [string trimright $synopsis \n] - lappend blank_header_col "" - } - - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - if {$use_table} { - set t [textblock::class::table new "$CLR(title)Usage$RST"] - $t add_column -headers $blank_header_col -minwidth 3 - $t add_column -headers $blank_header_col - - if {!$is_custom_argdisplay} { - lappend blank_header_col "" - #spanned columns in default argdisplay area - $t add_column -headers $blank_header_col ;#Default - $t add_column -headers $blank_header_col ;#Multi - $t add_column -headers $blank_header_col ;#Help - set arg_colspans {1 4 0 0 0} - } else { - if {$argdisplay_header ne ""} { - lappend blank_header_col "" - } - set arg_colspans {1 1} - } - } - set h 0 - if {$cmdname ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list COMMAND: $cmdname_display] - } else { - lappend errlines "COMMAND: $cmdname_display" - } - incr h - } - if {$cmdhelp ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Description: $cmdhelp_display] - } else { - lappend errlines "Description: $cmdhelp_display" - } - incr h - } - if {$docurl ne ""} { - if {![catch {package require punk::ansi}]} { - set docurl [punk::ansi::hyperlink $docurl] - } - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list $docname $docurl_display] - } else { - lappend errlines "$docname $docurl_display" - } - incr h - } - if {$synopsis ne ""} { - if {$use_table} { - $t configure_header $h -colspans $arg_colspans -values [list Synopsis: [punk::ansi::ansiwrap brightwhite $synopsis]] - } else { - #todo - lappend errlines "Synopsis:\n$synopsis" - } - incr h - } - - - if {$use_table} { - if {$is_custom_argdisplay} { - if {$argdisplay_header ne ""} { - $t configure_header $h -colspans {2 0} -values [list $argdisplay_header] - } - } else { - $t configure_header $h -values {Arg Type Default Multi Help} - } - } else { - lappend errlines " --ARGUMENTS-- " - } - - if {$is_custom_argdisplay} { - if {$use_table} { - #using overall container table - #header already added - #TODO - review textblock::table features - #we can't currently span columns within the table body. - #This feature could allow hidden data columns (and sort on hidden col?) - #potentially require coordination with header colspans? - $t add_row [list "" $argdisplay_body] - } else { - if {$argdisplay_header ne ""} { - lappend errlines $argdisplay_header - } - lappend errlines {*}$argdisplay_body - } - } else { - - #set A_DEFAULT [a+ brightwhite Brightgreen] - set A_DEFAULT "" - set A_BADARG $CLR(badarg) - set A_GOODARG $CLR(goodarg) - set A_GOODCHOICE $CLR(goodchoice) - set greencheck $CLR(check)\u2713$RST ;#green tick - set soloflag $CLR(solo)\u2690$RST ;#flag - may be replacement char in old dos prompt (?) - set A_PREFIX $CLR(choiceprefix) ;#use a+ so colour off can apply - if {$A_PREFIX eq "" || $A_PREFIX eq [a+ underline]} { - #A_PREFIX can resolve to empty string if colour off - #we then want to display underline instead - set A_PREFIX [a+ underline] - set A_PREFIXEND [a+ nounderline]\u200B ;#padding will take ANSI from last char - so add a zero width space - } else { - set A_PREFIXEND $RST - } - - #TODO - foreach fid - set fid [lindex $selected_forms 0] - set form_dict [dict get $spec_dict FORMS $fid] - - set opt_names [list] - set opt_names_display [list] - if {[llength [dict get $form_dict OPT_NAMES]]} { - if {![catch {package require punk::trie}]} { - set trie [punk::trie::trieclass new {*}[dict get $form_dict OPT_NAMES]] - set idents [dict get [$trie shortest_idents ""] scanned] - #todo - check opt_prefixdeny - - $trie destroy - foreach c [dict get $form_dict OPT_NAMES] { - set arginfo [dict get $form_dict ARG_INFO $c] - if {[dict get $arginfo -prefix]} { - set id [dict get $idents $c] - #REVIEW - if {$id eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $id] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - lappend opt_names_display $A_PREFIX$prefix$A_PREFIXEND$tail - } else { - lappend opt_names_display $c - } - #lappend opt_names_display $M[ansistring VIEW $prefix]$RST[ansistring VIEW $tail] - lappend opt_names $c - } - } else { - set opt_names [dict get $form_dict OPT_NAMES] - set opt_names_display $opt_names - } - } - set leading_val_names [dict get $form_dict LEADER_NAMES] - set trailing_val_names [dict get $form_dict VAL_NAMES] - - #dict for {argname info} [tcl::dict::get $form_dict arg_info] { - # if {![string match -* $argname]} { - # lappend leading_val_names [lpop trailing_val_names 0] - # } else { - # break - # } - #} - #if {![llength $leading_val_names] && ![llength $opt_names]} { - # #all vals were actually trailing - no opts - # set trailing_val_names $leading_val_names - # set leading_val_names {} - #} - set leading_val_names_display $leading_val_names - set trailing_val_names_display $trailing_val_names - #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 argumentset [list [list $leading_val_names_display $leading_val_names $parsed_leaders] [list $opt_names_display $opt_names $parsed_opts] [list $trailing_val_names_display $trailing_val_names $parsed_values]] { - lassign $argumentset argnames_display argnames parsedvalues - - foreach argshow $argnames_display arg $argnames { - set arginfo [dict get $form_dict ARG_INFO $arg] - if {[dict exists $arginfo -default]} { - set default "'$A_DEFAULT[dict get $arginfo -default]$RST'" - } else { - set default "" - } - set help [Dict_getdef $arginfo -help ""] - set allchoices_originalcase [list] - set choices [Dict_getdef $arginfo -choices {}] - set choicegroups [Dict_getdef $arginfo -choicegroups {}] - set choicemultiple [dict get $arginfo -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set choicecolumns [Dict_getdef $arginfo -choicecolumns 4] - set choiceprefixdenylist [Dict_getdef $arginfo -choiceprefixdenylist {}] - if {[Dict_getdef $arginfo -multiple 0]} { - set multiple $greencheck - set is_multiple 1 - } else { - set multiple "" - set is_multiple 0 - } - if {[dict exists $choicegroups ""]} { - dict lappend choicegroups "" {*}$choices - } else { - set choicegroups [dict merge [dict create "" $choices] $choicegroups] - } - dict for {groupname clist} $choicegroups { - lappend allchoices_originalcase {*}$clist - } - set has_choices [expr {[dict exists $arginfo -choices] || [dict exists $arginfo -choicegroups]}] - - if {$has_choices} { - if {$help ne ""} {append help \n} - if {[dict get $arginfo -nocase]} { - set casemsg " (case insensitive)" - set allchoices_test [string tolower $allchoices_originalcase] - } else { - set casemsg " (case sensitive)" - set allchoices_test $allchoices_originalcase - } - if {[dict get $arginfo -choiceprefix]} { - set prefixmsg " (choice prefix allowed)" - } else { - set prefixmsg "" - } - set choicelabeldict [Dict_getdef $arginfo -choicelabels {}] - set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}] - set formattedchoices [dict create] ;#use dict rather than array to preserve order - append help " Choices$prefixmsg$casemsg" - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - if {$choicemultiple_max == -1} { - append help \n " The value can be a list of $choicemultiple_min or more of these choices" - } else { - if {$choicemultiple_min eq $choicemultiple_max} { - append help \n " The value must be a list of $choicemultiple_min of these choices" - } else { - append help \n " The value can be a list of $choicemultiple_min to $choicemultiple_max of these choices" - } - } - } - if {![dict get $arginfo -choiceprefix] || [catch {package require punk::trie}]} { - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - #set formattedchoices [dict get $arginfo -choices] - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - } else { - if {[catch { - set trie [punk::trie::trieclass new {*}$allchoices_test] - set idents [dict get [$trie shortest_idents ""] scanned] - if {[dict get $arginfo -nocase]} { - #idents were calculated on lcase - remap keys in idents to original casing - set actual_idents $idents - foreach ch $allchoices_originalcase { - if {![dict exists $idents $ch]} { - #don't need to adjust the capitalisation in the value to match the key -as only length is used for highlighting - #The actual testing is done in get_dict - dict set actual_idents $ch [dict get $idents [string tolower $ch]] - } - } - set idents $actual_idents - #puts "-----" - #puts "idents $idents" - } - - $trie destroy - dict for {groupname clist} $choicegroups { - foreach c $clist { - if {$c in $choiceprefixdenylist} { - set shortestid $c - } else { - set shortestid [dict get $idents $c] - } - if {$shortestid eq $c} { - set prefix $c - set tail "" - } else { - set idlen [string length $shortestid] - set prefix [string range $c 0 $idlen-1] - set tail [string range $c $idlen end] - } - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set mk " [join $markers {}]" - } else { - set mk "" - } - set cdisplay "$A_PREFIX[ansistring VIEW $prefix]$A_PREFIXEND[ansistring VIEW $tail]$mk" - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } errM]} { - #this failure can happen if -nocase is true and there are ambiguous entries - #e.g -nocase 1 -choices {x X} - puts stderr "prefix marking failed\n$errM" - #append help "\n " [join [dict get $arginfo -choices] "\n "] - if {[dict size $choicelabeldict]} { - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c [join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $choicelabeldict $c]} { - append cdisplay \n [dict get $choicelabeldict $c] - } - - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } else { - #set formattedchoices $choicegroups - dict for {groupname clist} $choicegroups { - foreach c $clist { - set markers [punk::args::lib::choiceinfo_marks $c $choiceinfodict] - if {[llength $markers]} { - set cdisplay "$c[join $markers {}]" - } else { - set cdisplay $c - } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { - dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } else { - dict lappend formattedchoices $groupname $cdisplay - } - } - } - } - - } - } - set choicetable_objects [list] - set choicetable_footers [dict create] - dict for {groupname formatted} $formattedchoices { - set numcols $choicecolumns ;#todo - dynamic? - if {[llength $formatted] < $numcols} { - #don't show blank cells if single line of results - set numcols [llength $formatted] - } - if {$numcols > 0} { - if {$use_table} { - #risk of recursing - #TODO -title directly in list_as_table - set choicetableobj [textblock::list_as_table -return tableobject -show_hseps 1 -show_edge 1 -columns $numcols $formatted] - lappend choicetable_objects $choicetableobj - $choicetableobj configure -title $CLR(groupname)$groupname - #append help \n[textblock::join -- " " [$choicetableobj print]] - } else { - if {$groupname ne ""} { - append help \n \n "$CLR(groupname)Group: $groupname$RST" - } else { - append help \n - } - append help \n [join $formatted \n] - } - } else { - #we were given an empty set of choices. - #probably an error in the definition - but could happen if dynamically generated. - #(e.g ensemble where unknown mechanism is used for subcommands?) - #better to just display that there were none rather than totally break the usage output. - if {$usetable} { - #these will be displayed after all table entries - if {$groupname eq ""} { - dict set choicetable_footers "" " $CLR(errormsg)(no choices defined for main group)$RST" - } else { - dict set choicetable_footers $groupname " $CLR(errormsg)(no choices defined for group $groupname)$RST" - } - } else { - if {$groupname eq ""} { - append help \n " " $CLR(errormsg)(no choices defined)$RST - } else { - append help \n " " $CLR(errormsg)(no choices defined for group $groupname)$RST - } - } - } - } - set twidths_by_colcount [dict create] ;#to set all subtables with same colcount to same width - foreach obj $choicetable_objects { - dict lappend twidths_by_colcount [$obj column_count] [$obj width] - } - foreach obj $choicetable_objects { - set cols [$obj column_count] - set widths [dict get $twidths_by_colcount $cols] - set max [tcl::mathfunc::max {*}$widths] - $obj configure -minwidth $max ;#expand smaller ones - set i 0 - while {$i < $cols} { - #keep text aligned left on expanded tables - $obj configure_column $i -blockalign left - incr i - } - - append help \n[textblock::join -- " " [$obj print]] - #set ansititle [dict get [$obj configure -title] value] - $obj destroy - } - if {[dict size $choicetable_footers]} { - foreach groupname [dict keys $formattedchoices] { - if {[dict exists $choicetable_footers $groupname]} { - append help \n [dict get $choicetable_footers $groupname] - } - } - } - - #review. use -type to restrict additional choices - may be different to values in the -choices - if {![dict get $arginfo -choicerestricted]} { - #when -choicemultiple - the -type refers to each selection - if {[dict get $arginfo -type] eq "string"} { - append help "\n (values not in defined choices are allowed)" - } else { - append help "\n (values not in defined choices are allowed but must by of type: [dict get $arginfo -type])" - } - } - } - if {[Dict_getdef $arginfo -optional 0] == 1 || [dict exists $arginfo -default]} { - if {$is_multiple} { - set argshow "?${argshow}...?" - } else { - set argshow "?${argshow}?" - } - } else { - if {$is_multiple} { - set argshow "${argshow}..." - } - } - set typeshow [dict get $arginfo -type] - if {$typeshow eq "none"} { - set typeshow "$typeshow $soloflag" - } - if {[dict exists $arginfo -minsize]} { - append typeshow \n "-minsize [dict get $arginfo -minsize]" - } - if {[dict exists $arginfo -maxsize]} { - append typeshow \n "-maxsize [dict get $arginfo -maxsize]" - } - if {[dict exists $arginfo -range]} { - append typeshow \n "-range [dict get $arginfo -range]" - } - - if {$use_table} { - $t add_row [list $argshow $typeshow $default $multiple $help] - if {$arg eq $badarg} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { - $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG - } - } else { - #review - formatting will be all over the shop due to newlines in typesshow, help - set arghelp "[a+ bold]$argshow$RST TYPE:$typeshow DEFAULT:$default MULTI:$multiple\n" - if {$arg eq $badarg} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { - set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] - } - foreach ln [split $help \n] { - append arghelp " $ln" \n - } - lappend errlines $arghelp - } - } - } - } ;#end is_custom_argdisplay - - if {$use_table} { - $t configure -show_hseps 0\ - -show_header 1\ - -ansibase_body $CLR(ansibase_body)\ - -ansibase_header $CLR(ansibase_header)\ - -ansiborder_header $CLR(ansiborder)\ - -ansiborder_body $CLR(ansiborder) - - $t configure -maxwidth 80 ;#review - if {$returntype ne "tableobject"} { - append errmsg [$t print] - #returntype of table means just the text of the table - $t destroy - } - } else { - append errmsg [join $errlines \n] - } - } errM]} { - catch {$t destroy} - append errmsg \n - append errmsg "(additional error in punk::args::arg_error when attempting to display usage)" \n - append errmsg "$errM" \n - append errmsg "$::errorInfo" - - } - set arg_error_isrunning 0 - if {$use_table} { - #assert returntype is one of table, tableobject - set result $errmsg ;#default if for some reason table couldn't be used - if {$returntype eq "tableobject"} { - if {[info object isa object $t]} { - set result $t - } - } - } else { - set result $errmsg - } - if {$as_error} { - #add PUNK to the tail end of the more usual -errorcode {TCL WRONGARGS} so we maintain reasonable compat with things looking for TCL WRONGARGS - but also differentiate it. - #Also, we're polite enough in the errorInfo, nothing wrong with a Clint Eastwood style errorCode ;) - uplevel 1 [list return -code error -errorcode {TCL WRONGARGS PUNK} $result] - } else { - return $result - } - } - - - lappend PUNKARGS [list { - @dynamic - @id -id ::punk::args::usage - @cmd -name punk::args::usage -help\ - "Return usage information for a command identified by an id. - - This will only work for commands where a punk::args definition exists - for the command and an id has been defined for it. The id for custom - help for a command should match the fully qualified name of the command. - - Many commands (such as ensembles and oo objects) may have argument - documentation generated dynamically and may not yet have an id. - IDs for autogenenerated help are prefixed e.g (autodef)::myensemble. - - Generally punk::ns::arginfo (aliased as i in the punk shell) should - be used in preference - as it will search for a documentation - mechanism and call punk::args::usage as necessary. - " - -return -default table -choices {string table tableobject} - }\ - {${[punk::args::resolved_def -types opts -override {-scheme {-default info}} ::punk::args::arg_error -scheme]}}\ - {${[punk::args::resolved_def -types opts ::punk::args::resolved_def -form]}}\ - { - - @values -min 0 -max 1 - id -help\ - "Exact id. - Will usually match the command name" - }] - proc usage {args} { - #lassign [dict values [punk::args::get_by_id ::punk::args::usage $args]] leaders opts values received - lassign [dict values [punk::args::parse $args withid ::punk::args::usage]] leaders opts values received - set id [dict get $values id] - set real_id [real_id $id] - if {$real_id eq ""} { - error "punk::args::usage - no such id: $id" - } - #-scheme punk_info ?? - arg_error "" [punk::args::get_spec $real_id] {*}$opts -aserror 0 - } - - lappend PUNKARGS [list { - @id -id ::punk::args::get_by_id - @cmd -name punk::args::get_by_id - @values -min 1 - id - arglist -type list -help\ - "list containing arguments to be parsed as per the - argument specification identified by the supplied id." - }] - - - #deprecate? - proc get_by_id {id arglist} { - set definitionlist [punk::args::raw_def $id] - if {[llength $definitionlist] == 0} { - error "punk::args::get_by_id - no such id: $id" - } - #uplevel 1 [list ::punk::args::get_dict {*}$definitionlist $arglist] - tailcall ::punk::args::get_dict $definitionlist $arglist - } - - #consider - - #require eopts indicator -- ? (because first or only arg in arglist could be flaglike and match our own) - #parse ?-flag val?... -- $arglist withid $id - #parse ?-flag val?... -- $arglist withdef $def ?$def?... - - #an experiment.. ideally we'd like arglist at the end? - #parse_withid ?-flag val?.. $id $arglist - #parse_withdef ?-flag val?.. -- $def ?$def?... $arglist ;#error prone syntax? - #no possible equivalent for parse_withdef ??? - - lappend PUNKARGS [list { - @id -id ::punk::args::parse - @cmd -name punk::args::parse -help\ - "parse and validate command arguments based on a definition. - - In the 'withid' form the definition is a pre-existing record that has been - created with ::punk::args::define, or indirectly by adding a definition to - the PUNKARGS variable in a namespace which is then registered in - punk::args::register::NAMESPACES, or by a previous call to punk::parse - using 'withdef' and a definition block containing an @id -id directive. - - In the 'withdef' form - the definition is created on the first call and - cached thereafter, if the id didn't already exist. - - form1: parse $arglist ?-flag val?... withid $id - form2: parse $arglist ?-flag val?... withdef $def ?$def? - see punk::args::define - - Returns a dict of information regarding the parsed arguments - example of basic usage for single option only: - punk::args::define { - @id -id ::myns::myfunc - @cmd -name myns::myfunc - @leaders -min 0 -max 0 - @opts - -configfile -type existingfile - #type none makes it a solo flag - -verbose -type none - @values -min 0 -max 0 - } - proc myfunc {args} { - set argd [punk::args::parse $args withid ::myns::myfunc] - lassign [dict values $argd] leaders opts values received solos - if {[dict exists $received] -configfile} { - puts \"have option for existing file [dict get $opts -configfile]\" - } - } - The leaders, opts, values keys in the parse result dict are proper dicts. - The received key is dict-like but can have repeated keys for arguments than can - accept multiples. The value for each received element is the ordinal position. - The solos key refers to a list of solo flags received (those specified with - -type none). This is generally only useful to assist in passing arguments on - to another procedure which also requires solos, because the opts dict contains - solo flags with a 1 value or a list of 1's if it was a solo with -multiple true - specified. - " - @form -form {withid withdef} - @leaders -min 1 -max 1 - arglist -type list -optional 0 -help\ - "Arguments to parse - supplied as a single list" - - @opts - -form -type list -default * -help\ - "Restrict parsing to the set of forms listed. - Forms are the orthogonal sets of arguments a - command can take - usually described in 'synopsis' - entries." - #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} - - @values -min 2 - - @form -form withid -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withid $id" - @values -max 2 - withid -type literal -help\ - "The literal value 'withid'" - id -type string -help\ - "id of punk::args definition for a command" - - - @form -form withdef -synopsis "parse arglist ?-form {int|...}? ?-errorstyle ? withdef $def ?$def?" - withdef -type literal -help\ - "The literal value 'withdef'" - - #todo - make -dynamic obsolete - use @dynamic directive instead - def -type string -multiple 1 -optional 0 -help\ - "Each remaining argument is a block of text - defining argument definitions. - As a special case, -dynamic may be - specified as the 1st 2 arguments. These are - treated as an indicator to punk::args about - how to process the definition." - - }] - proc parse {args} { - set tailtype "" ;#withid|withdef - if {[llength $args] < 3} { - #error "punk::args::parse - invalid call. < 3 args" - punk::args::parse $args withid ::punk::args::parse - } - set opts_and_vals $args - set parseargs [lpop opts_and_vals 0] - - set opts [list] - set values [list] - for {set i 0} {$i < [llength $opts_and_vals]} {incr i} { - if {[string match -* [lindex $opts_and_vals $i]]} { - if {[catch { - lappend opts [lpop opts_and_vals 0] [lpop opts_and_vals 0] - }]} { - #unhappy path - not enough options - #review - which form of punk::args::parse? - punk::args::parse $args withid ::punk::args::parse - } - incr i -1 - #lappend opts $a [lindex $opts_and_vals $i] - } else { - break - } - } - #set values [lrange $opts_and_vals $i end] - set values $opts_and_vals - #puts "---values: $values" - set tailtype [lindex $values 0] - set tailargs [lrange $values 1 end] - - - #set split [lsearch -exact $tailargs withid] - #if {$split < 0} { - # set split [lsearch -exact $tailargs withdef] - # if {$split < 0} { - # #punk::args::usage arg_error? - # #error "punk::args::parse - invalid call. keyword withid|withdef required" - # punk::args::parse $args withid ::punk::args::parse - # } else { - # set tailtype withdef - #} - #} else { - # set tailtype withid - #} - #set opts [lrange $tailargs 0 $split-1] ;#repeated flags will override earlier. That's ok here. - - - #if {[llength $opts] % 2} { - #error "punk::args::parse Even number of -flag val pairs required after arglist" - #} - - #Default the -errorstyle to enhanced - # (slowest on unhappy path - but probably clearest for playing with new APIs interactively) - # - application devs should distribute a config file with an errorstyle override if desired. - # - devs who prefer a different default for interactive use should create a config for it. (todo) - set defaultopts [dict create\ - -form {*}\ - -errorstyle enhanced\ - ] - - #todo - load override_errorstyle from configuration - #dict set defaultopts -errorstyle $ - #puts "def: $defaultopts opts: $opts" - set opts [dict merge $defaultopts $opts] - dict for {k v} $opts { - switch -- $k { - -form - -errorstyle { - } - default { - #punk::args::usage $args withid ::punk::args::parse ?? - #error "punk::args::parse unrecognised option $k. Known options [dict keys $defaultopts]" - punk::args::parse $args withid ::punk::args::parse - } - } - } - switch -- $tailtype { - withid { - if {[llength $tailargs] != 1} { - #error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - punk::args::parse $args withid ::punk::args::parse - } - set id [lindex $tailargs 0] - #puts stdout "punk::args::parse [llength $parseargs] args withid $id, options: $opts" - set deflist [raw_def $id] - if {[llength $deflist] == 0} { - error "punk::args::parse - no such id: $id" - } - } - withdef { - set deflist $tailargs - if {[llength $deflist] < 1} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - #puts stdout "punk::args::parse [llength $parseargs] args with [llength $deflist] definition blocks, options: $opts" - } - default { - error "punk::args::parse - invalid call. Argument following arglist was '$tailtype'. Must be 'withid' or 'withdef'" - } - } - try { - set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] - } trap {PUNKARGS VALIDATION} {msg erroropts} { - set opt_errorstyle [dict get $opts -errorstyle] - - #samples from get_dict (review: -argspecs can be *large* especially for multi-form argument definitions) - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname -argspecs $argspecs]] $msg - #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - - - 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] - switch -- $opt_errorstyle { - minimal { - return -options [list -code error -errorcode $ecode] $msg - } - basic { - #No table layout - unix manpage style - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -return string -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - standard { - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - } - return -options [list -code error -errorcode $ecode] $msg - } - enhanced { - set estack [info errorstack] ;#save it before we do anything to replace it (like the catch below) - set customdict [lrange $ecode 3 end] - set argspecs [Dict_getdef $customdict -argspecs ""] - set badarg [Dict_getdef $customdict -badarg ""] - set ecode_summary [lrange $ecode 0 2] - if {$badarg ne ""} { - lappend ecode_summary -badarg $badarg - } - catch {package require punk::lib} - if {[package provide punk::lib] ne ""} { - append msg \n [punk::lib::showdict -roottype list $estack */*] - } - if {$argspecs ne ""} { - set msg [arg_error $msg $argspecs -aserror 0 -badarg $badarg -form [dict get $opts -form]] - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } else { - #why? todo? - append msg \n "(enhanced error information unavailable)" - append msg \n "::errorCode summary: $ecode_summary" - return -options [list -code error -errorcode $ecode] $msg - } - } - debug { - puts stderr "errorstyle debug not implemented" - return -options [list -code error -errorcode $ecode] $msg - } - default { - puts stderr "errorstyle $opt_errorstyle not recognised: expected one of minimal basic standard enhanced debug" - return -options [list -code error -errorcode $ecode] $msg - } - } - } trap {PUNKARGS} {msg erropts} { - append msg \n "Unexpected PUNKARGS error" - return -options [list -code error -errorcode $ecode] $msg - } trap {} {msg erroropts} { - #review - #quote from DKF: The reason for using return -code error vs error or throw depends on where the error is. If the problem is in your code, use error or throw. - #If the problem is in your caller (e.g., because they gave you bad arguments) then use return -code error. Simple. - throw [dict get $erroropts -errorcode] [dict get $erroropts -errorinfo] - } - return $result - } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - - #todo? - a version of get_dict that directly supports punk::lib::tstr templating - #rename get_dict - # - - #generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values - #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. - #only supports -flag val pairs, not solo options - #If an option is supplied multiple times - only the last value is used. - proc get_dict {deflist rawargs args} { - #see arg_error regarding considerations around unhappy-path performance - - if {[llength $args] % 2 != 0} { - error "punk::args::get_dict args must be a dict of option value pairs" - } - set defaults [dict create\ - -form *\ - ] - set opts [dict merge $defaults $args] - dict for {k v} $opts { - switch -- $k { - -form {} - default { - error "punk::args::get_dict Unexpected option '$k' Known options -form" - } - } - } - - - #*** !doctools - #[call [fun get_dict] [arg deflist] [arg rawargs] [arg args]] - #[para]Parse rawargs as a sequence of zero or more option-value pairs followed by zero or more values - #[para]Returns a dict of the form: opts values - #[para]ARGUMENTS: - #[list_begin arguments] - #[arg_def list-of-multiline-string deflist] - #[para] These are blocks of text with records delimited by newlines (lf or crlf) - but with multiline values allowed if properly quoted/braced - #[para]'info complete' is used to determine if a record spans multiple lines due to multiline values - #[para]Each optionspec line defining a flag must be of the form: - #[para]-optionname -key val -key2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices -optional - #[para]Each optionspec line defining a positional argument is of the form: - #[para]argumentname -key val -ky2 val2... - #[para]where the valid keys for each option specification are: -default -type -range -choices - #[para]comment lines begining with # are ignored and can be placed anywhere except within a multiline value where it would become part of that value - #[para]lines beginning with @cmd @leaders @opts or @values also take -key val pairs and can be used to set defaults and control settings. - #[para]@opts or @values lines can appear multiple times with defaults affecting flags/values that follow. - #[arg_def list rawargs] - #[para] This is a list of the arguments to parse. Usually it will be the $args value from the containing proc, - #but it could be a manually constructed list of values made for example from positional args defined in the proc. - #[list_end] - #[para] - - #consider line-processing example below for which we need info complete to determine record boundaries - #punk::args::get_dict [list { - # @opts - # -opt1 -default {} - # -opt2 -default { - # etc - # } - # @values -multiple 1 - #}] $args - - - - - #rawargs: args values to be parsed - #we take a definition list rather than resolved argspecs - because the definition could be dynamic - - #if definition has been seen before, - #define will either return a permanently cached argspecs (-dynamic 0) - or - # use a cached pre-split definition with parameters to dynamically generate a new (or limitedly cached?) argspecs. - set argspecs [uplevel 1 [list ::punk::args::resolve {*}$deflist]] - - # ----------------------------------------------- - # Warning - be aware of all vars thrown into this space (from tail end of 'definition' proc) - tcl::dict::with argspecs {} ;#turn keys into vars - #e.g id,FORMS,cmd_info,doc_info,package_info,seealso_info,id_info,form_names - # ----------------------------------------------- - set opt_form [dict get $opts -form] - if {$opt_form eq "*"} { - set selected_forms $form_names - } elseif {[string is integer -strict $opt_form]} { - if {$opt_form < 0 || $opt_form > [llength $form_names]-1} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list [lindex $form_names $opt_form]] - } else { - if {$opt_form ni $form_names} { - error "punk::args::get_dict invalid -form value '$opt_form' Expected int 0-[expr {[llength $form_names]-1}] or one of '$form_names'" - } - set selected_forms [list $opt_form] - } - - - #puts "-arg_info->$arg_info" - set flagsreceived [list] ;#for checking if required flags satisfied - set solosreceived [list] - set multisreceived [list] - #secondary purpose: - #for -multple true, we need to ensure we can differentiate between a default value and a first of many that happens to match the default. - #-default value must not be appended to if argname not yet in flagsreceived - - - #todo: -minmultiple -maxmultiple ? - - # -- --- --- --- - # Handle leading positionals - # todo - consider allowing last leading positional to have -multiple 1 but only if there exists an eopts marker later (--) ? - - #todo - handle multiple fids? - set fid [lindex $selected_forms 0] - set formdict [dict get $FORMS $fid] - tcl::dict::with formdict {} - #populate vars ARG_INFO,LEADER_MAX,LEADER_NAMES etc - if {$VAL_MIN eq ""} { - set valmin 0 - set VAL_MIN 0 - } else { - set valmin $VAL_MIN - } - - set pre_values {} - - set argnames [tcl::dict::keys $ARG_INFO] - set optnames [lsearch -all -inline $argnames -*] - set ridx 0 - set rawargs_copy $rawargs - set leader_posn_name "" - set leader_posn_names_assigned [dict create] ;#track if the name got a value (or multiple if last one) - set is_multiple 0 ;#last leader may be multi - if {$LEADER_MAX != 0} { - foreach r $rawargs_copy { - if {$LEADER_MAX ne "" && $LEADER_MAX != -1 && $ridx > $LEADER_MAX-1} { - break - } - if {$ridx == [llength $LEADER_NAMES]-1} { - #at last named leader - set leader_posn_name [lindex $LEADER_NAMES $ridx] - if {[dict exists $ARG_INFO $leader_posn_name -multiple] && [dict get $ARG_INFO $leader_posn_name -multiple]} { - set is_multiple 1 - } - } elseif {$ridx > [llength $LEADER_NAMES]-1} { - #beyond names - retain name if -multiple was true - if {!$is_multiple} { - set leader_posn_name "" - } - } else { - set leader_posn_name [lindex $LEADER_NAMES $ridx] ;#may return empty string - } - if {$r eq "--"} { - #review end of opts marker: '--' can't be a leader (but can be a value) - break - } - - #argument such as a dictionary may have leading dash - test for whitespace to exclude as possible option - if {[tcl::string::match -* $r] && !([tcl::string::first " " $r]>=0 || [tcl::string::first \t $r]>=0 || [tcl::string::last \n $r]>=0)} { - set matchopt [::tcl::prefix::match -error {} $optnames $r] - if {$matchopt ne ""} { - #flaglike matches a known flag - don't treat as leader - break - } - #if {![string match -* [lindex $argnames $ridx]]} {} - if {$leader_posn_name ne ""} { - #there is a named leading positional for this position - #The flaglooking value doesn't match an option - so treat as a leader - foreach v $leader_posn_name { - lappend pre_values [lpop rawargs 0] - } - dict incr leader_posn_names_assigned $leader_posn_name - incr ridx - continue - } else { - break - } - } - - #for each branch - break or lappend - if {$leader_posn_name ne ""} { - set stridelength [llength $leader_posn_name] - if {$leader_posn_name ni $LEADER_REQUIRED} { - #optional leader - - #most adhoc arg processing will allocate based on number of args rather than matching choice values first - #(because a choice value could be a legitimate data value) - - #review - option to process in this manner? - #first check if the optional leader value is a match for a choice ? - #if {[dict exists $arg_info $leader_posn_name -choices]} { - # set vmatch [tcl::prefix match -error "" [dict get $arg_info $leader_posn_name -choices] [lindex $rawargs 0]] - # if {$vmatch ne ""} { - # #If we match a choice for this named position - allocated it regardless of whether enough args for trailing values - # lappend pre_values [lpop rawargs 0] - # incr ridx - # continue - # } - #} - - #check if enough rawargs to fill any required values - if {$VAL_MIN > 0 && [llength $rawargs] - $stridelength <= $VAL_MIN || [llength $rawargs] - $stridelength <= [llength $VAL_REQUIRED]} { - break - } else { - #leadername may be a 'stride' of arbitrary length (e.g {"key val"} or {"key val etc"}) - foreach v {$leader_posn_name} { - lappend pre_values [lpop rawargs 0] - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #required - if {[dict exists $leader_posn_names_assigned $leader_posn_name]} { - #already accepted at least one value - requirement satisfied - now equivalent to optional - if {$VAL_MIN > 0 && [llength $rawargs] - $stridelength <= $VAL_MIN || [llength $rawargs] - $stridelength <= [llength $VAL_REQUIRED]} { - break - } - } - #if we didn't break - requirement is not yet satisfied, or is satisfied but stil enough rawargs for required values - foreach v {$leader_posn_name} { - lappend pre_values [lpop rawargs 0] - } - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - #unnamed leader - if {$LEADER_MIN ne "" } { - if {$ridx > $LEADER_MIN-1} { - if {$LEADER_MAX ne "" && $ridx == $LEADER_MAX} { - break - } else { - if {$VAL_MIN ne ""} { - if {[llength $rawargs] > $VAL_MIN} { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } else { - break - } - } else { - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } - } else { - #haven't reached LEADER_MIN - lappend pre_values [lpop rawargs 0] - dict incr leader_posn_names_assigned $leader_posn_name - } - } else { - break - } - } - - incr ridx - } ;# end foreach r $rawargs_copy - } - - set argstate $ARG_INFO ;#argstate may have entries added - set arg_checks $ARG_CHECKS - - #JJJJ - if {$LEADER_MIN eq ""} { - set leadermin 0 - } else { - set leadermin $LEADER_MIN - } - if {$LEADER_MAX eq ""} { - set leadermax -1 - } else { - set leadermax $LEADER_MAX - } - - if {$VAL_MAX eq ""} { - set valmax -1 - } else { - set valmax $VAL_MAX - } - - #assert leadermax leadermin are numeric - #assert - rawargs has been reduced by leading positionals - - set opts [dict create] ;#don't set to OPT_DEFAULTS here - - - set leaders [list] - set arglist {} - set post_values {} - #valmin, valmax - #puts stderr "rawargs: $rawargs" - #puts stderr "argstate: $argstate" - if {[lsearch $rawargs -*] >= 0} { - #at least contains flaglike things.. - set maxidx [expr {[llength $rawargs] -1}] - if {$valmax == -1} { - set vals_total_possible [llength $rawargs] - set vals_remaining_possible $vals_total_possible - } else { - set vals_total_possible $valmax - set vals_remaining_possible $vals_total_possible - } - for {set i 0} {$i <= $maxidx} {incr i} { - set a [lindex $rawargs $i] - set remaining_args_including_this [expr {[llength $rawargs] - $i}] - #lowest valmin is 0 - if {$remaining_args_including_this <= $valmin} { - # if current arg is -- it will pass through as a value here - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - #exlude argument with whitespace from being a possible option e.g dict - if {[tcl::string::match -* $a] && !([tcl::string::first " " $a]>=0 || [tcl::string::first \t $a]>=0 || [tcl::string::last \n $a]>=0)} { - if {$a eq "--"} { - #remaining num args <= valmin already covered above - if {$valmax != -1} { - #finite max number of vals - if {$remaining_args_including_this == $valmax} { - #assume it's a value. - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - } else { - #assume it's an end-of-options marker - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - } else { - #unlimited number of post_values accepted - #treat this as eopts - we don't care if remainder look like options or not - lappend flagsreceived -- - set arglist [lrange $rawargs 0 $i] - set post_values [lrange $rawargs $i+1 end] - } - break - } else { - set fullopt [tcl::prefix match -error "" $OPT_NAMES $a] - if {$fullopt ne ""} { - if {![tcl::dict::get $argstate $fullopt -prefix] && $a ne $fullopt} { - #attempt to use a prefix when not allowed - #review - by ending opts here - we dont' get the clearest error msgs - # may *sometimes* be better to raise a PUNKARGS VALIDATION (invalidoption) error - # (but it may actually be the first value that just happens to be flaglike) - #todo - check for subsequent valid flags or -- marker? - #consider for example 'file delete -f -- old.txt' - #If we just end option-processing, the punk::args parser would pass {-f -- old.txt} as values - #whereas the builtin file arg parser alerts that -f is a bad option - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #set arglist [lrange $rawargs 0 $i-1] - #set post_values [lrange $rawargs $i end] - #break - } - if {[tcl::dict::get $argstate $fullopt -type] ne "none"} { - #non-solo - #check if it was actually a value that looked like a flag - if {$i == $maxidx} { - #if no optvalue following - assume it's a value - #(caller should probably have used -- before it) - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - - set flagval [lindex $rawargs $i+1] - if {[tcl::dict::get $argstate $fullopt -multiple]} { - #don't lappend to default - we need to replace if there is a default - if {$fullopt ni $flagsreceived} { - tcl::dict::set opts $fullopt [list $flagval] - } else { - tcl::dict::lappend opts $fullopt $flagval - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt $flagval - } - #incr i to skip flagval - incr vals_remaining_possible -2 - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last option $fullopt at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $fullopt index [expr {$i-1}]] -badarg $fullopt -argspecs $argspecs]] $msg - } - } else { - #solo - if {[tcl::dict::get $argstate $fullopt -multiple]} { - if {$fullopt ni $flagsreceived} { - #override any default - don't lappend to it - tcl::dict::set opts $fullopt 1 - } else { - tcl::dict::lappend opts $fullopt 1 - } - if {$fullopt ni $multisreceived} { - lappend multisreceived $fullopt - } - } else { - tcl::dict::set opts $fullopt 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $fullopt ;#dups ok - } - lappend flagsreceived $fullopt ;#dups ok - } else { - #unmatched option flag - #comparison to valmin already done above - if {$valmax ne -1 && $remaining_args_including_this <= $valmax} { - #todo - look at optspec_default and see if solo/vs opt-val pair - #we may need to lookahead by 2 regarding valmax valmin - - #even with optany - assume an unknown within the space of possible values is a value - #unmatched option in right position to be considered a value - treat like eopts - #review - document that an unspecified arg within range of possible values will act like eopts -- - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - if {$OPT_ANY} { - set newval [lindex $rawargs $i+1] - #opt was unspecified but is allowed due to @opts -any 1 - 'adhoc/passthrough' option - tcl::dict::set argstate $a $OPTSPEC_DEFAULTS ;#use default settings for unspecified opt - tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS - if {[tcl::dict::get $argstate $a -type] ne "none"} { - if {[tcl::dict::get $argstate $a -multiple]} { - tcl::dict::lappend opts $a $newval - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a $newval - } - if {[incr i] > $maxidx} { - set msg "Bad options for %caller%. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingoptionvalue $a index [expr {$i-1}]] -badarg $a -argspecs $argspecs]] $msg - #arg_error "punk::args::get_dict bad options for [Get_caller]. No value supplied for last adhoc option $a at index [expr {$i-1}] which is not marked with -type none" $argspecs -badarg $a - } - incr vals_remaining_possible -2 - } else { - #review -we can't provide a way to allow unspecified -type none flags through reliably/unambiguously unless all remaining unspecified options are -type none - if {[tcl::dict::get $argstate $a -multiple]} { - if {![tcl::dict::exists $opts $a]} { - tcl::dict::set opts $a 1 - } else { - tcl::dict::lappend opts $a 1 - } - if {$a ni $multisreceived} { - lappend multisreceived $a - } - } else { - tcl::dict::set opts $a 1 - } - incr vals_remaining_possible -1 - lappend solosreceived $a - } - lappend flagsreceived $a ;#adhoc flag as supplied - } else { - if {[llength $OPT_NAMES]} { - set errmsg "bad options for %caller%. Unexpected option \"$a\": must be one of: $OPT_NAMES" - } else { - set errmsg "bad options for %caller%. Unexpected option \"$a\": No options defined while @opts -any 0" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list invalidoption $a options $OPT_NAMES] -badarg $a -argspecs $argspecs]] $errmsg - #arg_error $errmsg $argspecs -badarg $fullopt - } - } - } - } else { - #not flaglike - set arglist [lrange $rawargs 0 $i-1] - set post_values [lrange $rawargs $i end] - break - } - } - #set values [list {*}$pre_values {*}$post_values] - set leaders $pre_values - set values $post_values - } else { - set leaders $pre_values - set values $rawargs - #set values [list {*}$pre_values {*}$rawargs] ;#no -flags detected - set arglist [list] - } - #puts stderr "--> arglist: $arglist" - #puts stderr "--> values: $values" - - #--------------------------------------- - set ordered_opts [dict create] - foreach o $OPT_NAMES { - if {[dict exists $opts $o]} { - dict set ordered_opts $o [dict get $opts $o] - } elseif {[dict exists $OPT_DEFAULTS $o]} { - dict set ordered_opts $o [dict get $OPT_DEFAULTS $o] - } - } - #add in possible '-any true' opts after the defined opts - dict for {o oval} $opts { - if {![dict exists $ordered_opts $o]} { - dict set ordered_opts $o $oval - } - } - set opts $ordered_opts - #--------------------------------------- - - - set positionalidx 0 ;#index for unnamed positionals (both leaders and values) - set leadername_multiple "" - set leadernames_received [list] - - set num_leaders [llength $leaders] - #---------------------------------------- - #set leaders_dict $LEADER_DEFAULTS ;#wrong - set leaders_dict [dict create] - foreach lname [lrange $LEADER_NAMES 0 $num_leaders-1] { - dict set leaders_dict $lname {} - } - set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] - #---------------------------------------- - - set start_position $positionalidx - set nameidx 0 - #MAINTENANCE - same loop logic as for values - for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} { - set leadername [lindex $LEADER_NAMES $nameidx] - incr nameidx - set ldr [lindex $leaders $ldridx] - if {$leadername ne ""} { - if {[llength $leadername] == 1} { - set strideval $ldr - } else { - set strideval [list] - incr ldridx -1 - foreach v $leadername { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $strideval] values for '$leadername', but requires [llength $leadername] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $leadername] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $leaders $ldridx] - } - } - - if {[tcl::dict::get $argstate $leadername -multiple]} { - if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - #current stored ldr equals defined default - don't include default in the list we build up - tcl::dict::set leaders_dict $leadername [list $strideval] ;#important to treat first element as a list - } else { - tcl::dict::lappend leaders_dict $leadername $strideval - } - set leadername_multiple $leadername - } else { - tcl::dict::set leaders_dict $leadername $strideval - } - lappend leadernames_received $leadername - } else { - if {$leadername_multiple ne ""} { - if {[llength $leadername_multiple] == 1} { - set strideval $ldr - } else { - set strideval [list] - incr ldridx -1 - foreach v $leadername_multiple { - incr ldridx - if {$ldridx > [llength $leaders]-1} { - set msg "Bad number of leaders for %caller%. Received [llength $strideval] values for '$leadername_multiple', but requires [llength $leadername_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $leadername_multiple] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $leaders $ldridx] - } - } - tcl::dict::lappend leaders_dict $leadername_multiple $strideval - #name already seen - but must add to leadernames_received anyway (as with opts and values) - lappend leadernames_received $leadername_multiple - } else { - tcl::dict::set leaders_dict $positionalidx $ldr - tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - lappend leadernames_received $positionalidx - } - } - set positionalidx [expr {$start_position + $ldridx + 1}] - } - - #test args parse_withdef_leader_stride - todo - #change to for loop - #foreach leadername $LEADER_NAMES ldr $leaders { - # if {$ldridx+1 > $num_leaders} { - # break - # } - # if {$leadername ne ""} { - # if {[tcl::dict::get $argstate $leadername -multiple]} { - # if {[tcl::dict::exists $LEADER_DEFAULTS $leadername]} { - # tcl::dict::set leaders_dict $leadername [list $ldr] ;#important to treat first element as a list - # } else { - # tcl::dict::lappend leaders_dict $leadername $ldr - # } - # set leadername_multiple $leadername - # } else { - # tcl::dict::set leaders_dict $leadername $ldr - # } - # lappend leadernames_received $leadername - # } else { - # if {$leadername_multiple ne ""} { - # tcl::dict::lappend leaders_dict $leadername_multiple $ldr - # lappend leadernames_received $leadername_multiple ;#deliberately allow dups! (as with opts and values) - # } else { - # tcl::dict::set leaders_dict $positionalidx $ldr - # tcl::dict::set argstate $positionalidx $LEADERSPEC_DEFAULTS - # tcl::dict::set arg_checks $positionalidx $LEADER_CHECKS_DEFAULTS - # lappend leadernames_received $positionalidx - # } - # } - # incr ldridx - # incr positionalidx - #} - - - set validx 0 - set valname_multiple "" - set valnames_received [list] - - set num_values [llength $values] - #------------------------------------------ - #!!! review - ## Don't set values_dict to VAL_DEFAULTS - or order of values_dict will be intermittently wrong based on whether values have defaults - ## set values_dict $val_defaults - set values_dict [dict create] - foreach valname [lrange $VAL_NAMES 0 $num_values-1] { - dict set values_dict $valname {} - } - set values_dict [dict merge $values_dict $VAL_DEFAULTS] - #------------------------------------------ - set nameidx 0 - set start_position $positionalidx - #MAINTENANCE - same loop logic as for leaders - for {set validx 0} {$validx < [llength $values]} {incr validx} { - set valname [lindex $VAL_NAMES $nameidx] - incr nameidx - set val [lindex $values $validx] - if {$valname ne ""} { - if {[llength $valname] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname', but requires [llength $valname] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - - if {[tcl::dict::get $argstate $valname -multiple]} { - if {[tcl::dict::exists $VAL_DEFAULTS $valname]} { - #current stored val equals defined default - don't include default in the list we build up - tcl::dict::set values_dict $valname [list $strideval] ;#important to treat first element as a list - } else { - tcl::dict::lappend values_dict $valname $strideval - } - set valname_multiple $valname - } else { - tcl::dict::set values_dict $valname $strideval - } - lappend valnames_received $valname - } else { - if {$valname_multiple ne ""} { - if {[llength $valname_multiple] == 1} { - set strideval $val - } else { - set strideval [list] - incr validx -1 - foreach v $valname_multiple { - incr validx - if {$validx > [llength $values]-1} { - set msg "Bad number of values for %caller%. Received [llength $strideval] values for '$valname_multiple', but requires [llength $valname_multiple] values." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list stridevaluecount [llength $strideval] stride [llength $valname_multiple] ] -argspecs $argspecs]] $msg - } - lappend strideval [lindex $values $validx] - } - } - tcl::dict::lappend values_dict $valname_multiple $strideval - #name already seen - but must add to valnames_received anyway (as with opts and leaders) - lappend valnames_received $valname_multiple - } else { - tcl::dict::set values_dict $positionalidx $val - tcl::dict::set argstate $positionalidx $VALSPEC_DEFAULTS - tcl::dict::set arg_checks $positionalidx $VAL_CHECKS_DEFAULTS - lappend valnames_received $positionalidx - } - } - set positionalidx [expr {$start_position + $validx + 1}] - } - #------------------------------------------ - - if {$leadermax == -1} { - #only check min - if {$num_leaders < $leadermin} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { - if {$leadermin == $leadermax} { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected between $leadermin and $leadermax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg - } - } - } - - if {$valmax == -1} { - #only check min - if {$num_values < $valmin} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } else { - if {$num_values < $valmin || $num_values > $valmax} { - if {$valmin == $valmax} { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } else { - set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected between $valmin and $valmax inclusive" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg - } - } - } - - #assertion - opts keys are full-length option names if -anyopts was false or if the supplied option as a shortname matched one of our defined options - - - #opts explicitly marked as -optional 0 must be present - regardless of -anyopts (which allows us to ignore additional opts to pass on to next call) - #however - if -anyopts is true, there is a risk we will treat a shortened option name as matching our default - when it was intended for the next call - #We SHOULD? always require exact matches for all required opts to avoid this risk, even though an ultimately-called function may not require the full-length option-name REVIEW - #The aim is to allow a wrapper function to specify a default value for an option (and/or other changes/restrictions to a particular option or two) - without having to re-specify all the options for the underlying function. - #without full respecification in the wrapper - we can't know that a supplied prefix is unambiguous at the next level - #For this reason we need to pass on full-length opts for any defined options in the wrapper even if anyopts is true - - #safe interp note - struct::set difference ensemble could be c or tcl implementation and we don't have an option to call directly? - #example timing difference: - #struct::set difference {x} {a b} - #normal interp 0.18 u2 vs safe interp 9.4us - #if {[llength [set missing [struct::set difference $OPT_REQUIRED $flagsreceived]]]} { - # error "Required option missing for [Get_caller]. missing flags $missing are marked with -optional false - so must be present in full-length form" - #} - #if {[llength [set missing [struct::set difference $VAL_REQUIRED $valnames_received]]]} { - # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" - #} - #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength [set missing [punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { - set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg - #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - if {[llength [set missing [punklib_ldiff $OPT_REQUIRED $flagsreceived]]]} { - set msg "Required option missing for %caller%. missing flags: '$missing' are marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list optionmissing $missing received $flagsreceived] -argspecs $argspecs]] $msg - #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs - } - if {[llength [set missing [punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { - set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg - #arg_error "Required value missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs - } - - - #todo - truncate/summarize values in error messages - - #todo - allow defaults outside of choices/ranges - - #check types,ranges,choices - set opts_and_values [tcl::dict::merge $leaders_dict $opts $values_dict] - #set combined_defaults [tcl::dict::merge $VAL_DEFAULTS $OPT_DEFAULTS] ;#can be no key collisions - we don't allow a value key beginning with dash - opt names must begin with dash - #puts "---opts_and_values:$opts_and_values" - #puts "---argstate:$argstate" - tcl::dict::for {argname v} $opts_and_values { - set thisarg [tcl::dict::get $argstate $argname] - #set thisarg_keys [tcl::dict::keys $thisarg] - set thisarg_checks [tcl::dict::get $arg_checks $argname] - set is_multiple [tcl::dict::get $thisarg -multiple] - set is_allow_ansi [tcl::dict::get $thisarg -allow_ansi] - set is_validate_ansistripped [tcl::dict::get $thisarg -validate_ansistripped] - set is_strip_ansi [tcl::dict::get $thisarg -strip_ansi] - set has_default [tcl::dict::exists $thisarg -default] - if {$has_default} { - set defaultval [tcl::dict::get $thisarg -default] - } - set type [tcl::dict::get $thisarg -type] - set has_choices [expr {[tcl::dict::exists $thisarg -choices] || [tcl::dict::exists $thisarg -choicegroups]}] - set regexprepass [tcl::dict::get $thisarg -regexprepass] - set regexprefail [Dict_getdef $thisarg -regexprefail ""] ;#aliased to dict getdef in tcl9 - set validationtransform [tcl::dict::get $thisarg -validationtransform] - - - if {$is_multiple} { - set vlist $v - } else { - set vlist [list $v] - } - set vlist_original $vlist ;#retain for possible final strip_ansi - - #review - validationtransform - if {$is_validate_ansistripped} { - #validate_ansistripped 1 - package require punk::ansi - set vlist_check [list] - foreach e $vlist { - lappend vlist_check [punk::ansi::ansistrip $e] - } - } else { - #validate_ansistripped 0 - set vlist_check $vlist - } - - switch -- [Dict_getdef $thisarg -ARGTYPE unknown] { - leader { - set dname leaders_dict - set argclass "Leading argument" - } - option { - set dname opts - set argclass Option - } - value { - set dname values_dict - set argclass "Trailing argument" - } - default { - set dname "_unknown_" ;#NA - set argclass "Unknown argument" - } - } - #reduce our validation requirements by removing values which match defaultval or match -choices - #(could be -multiple with -choicerestriction 0 where some selections match and others don't) - if {$has_choices} { - #-choices must also work with -multiple - #todo -choicelabels - set choiceprefix [tcl::dict::get $thisarg -choiceprefix] - set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}] - set choicerestricted [tcl::dict::get $thisarg -choicerestricted] - set choicemultiple [tcl::dict::get $thisarg -choicemultiple] - if {[string is integer -strict $choicemultiple]} { - set choicemultiple [list $choicemultiple $choicemultiple] - } - lassign $choicemultiple choicemultiple_min choicemultiple_max - set nocase [tcl::dict::get $thisarg -nocase] - set choices [Dict_getdef $thisarg -choices {}] - set choicegroups [Dict_getdef $thisarg -choicegroups {}] - set allchoices $choices - if {[dict size $choicegroups]} { - dict for {groupname groupmembers} $choicegroups { - lappend allchoices {*}$groupmembers - } - } - #note we can legitimately have dups in allchoices - if a choice should be documented to display in multiple groups - #This means we have to be dedup for testing with tcl::prefix::match - or the duped entries won't accept prefixes - - - set idx 0 ;# - #leaders_dict/opts/values_dict $argname member has been populated with the actual entered choices - which might be prefixes - #assert llength $vlist == llength [dict get $dname $argname] - # (unless there was a default and the option wasn't specified) - set vlist_validate [list] - set vlist_check_validate [list] - foreach e $vlist e_check $vlist_check { - set allchoices_in_list 0 - if {$choicemultiple_max > 1 || $choicemultiple_max == -1} { - #vlist and vlist_check can be list of lists if -multiple and -choicemultiple - #each e represents 0 or more choice selections - set c_list $e - set c_check_list $e_check - #todo? check if entire list matches default? - } else { - #only one choice at a time - ensure single entry in c_list c_check_list - set c_list [list $e] - set c_check_list [list $e_check] - } - - - #----------------------------------- - #fast fail on the wrong number of choices - if {[llength $c_list] < $choicemultiple_min} { - set msg "$argclass $argname for %caller% requires at least $choicemultiple_min choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - if {$choicemultiple_max != -1 && [llength $c_list] > $choicemultiple_max} { - set msg "$argclass $argname for %caller% requires at most $choicemultiple_max choices. Received [llength $c_list] choices." - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choicecount [llength $c_list] minchoices $choicemultiple_min maxchoices $choicemultiple_max] -badarg $argname]] $msg - } - #----------------------------------- - - set choice_idx 0 ;#we need to overwrite raw-choice (which may be prefix) with a value from the choice list - foreach c $c_list c_check $c_check_list { - if {$nocase} { - set casemsg " (case insensitive)" - set choices_test [tcl::string::tolower $allchoices] - #Don't lcase the denylist - even in nocase mode! - #set choiceprefixdenylist [tcl::string::tolower $choiceprefixdenylist] - set v_test [tcl::string::tolower $c_check] - } else { - set casemsg " (case sensitive)" - set v_test $c_check - set choices_test $allchoices - } - set choice_in_list 0 - set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval when -choicemultiple could be a list? - if {!$matches_default} { - if {$choiceprefix} { - #can we handle empty string as a choice? It should just work - REVIEW/test - set choice_exact_match 0 - if {$c_check in $allchoices} { - #for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing - set chosen $c_check - set choice_in_list 1 - set choice_exact_match 1 - } elseif {$v_test in $choices_test} { - #assert - if we're here, nocase must be true - #we know choice is present as full-length match except for case - #now we want to select the case from the choice list - not the supplied value - #we don't set choice_exact_match - because we will need to override the optimistic existing val below - #review - foreach avail [lsort -unique $allchoices] { - if {[string match -nocase $c $avail]} { - set chosen $avail - } - } - #assert chosen will always get set - set choice_in_list 1 - } else { - #PREFIX check required - any 'chosen' here is not an exact match or it would have matched above. - #assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above. - #in this block we can treat empty result from prefix match as a non-match - if {$nocase} { - #nocase implies that our entered value doesn't have to match case of choices - - #but we would still like to select the best match if there are case-dups. - #e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete - # selecting Del will find Delete, del will match delete (and raise error) - # but DEL will also match delete rather than Delete - so again an error is raised. - #This is counterintuitive with -nocase - #This is probably such an edge case that best served with documentation as a feature-not-bug - #Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete? - #The choice of the user to use -choiceprefixdenylist along with case-dups is the issue. - - set bestmatch [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$bestmatch eq ""} { - set chosen [tcl::prefix::match -error "" [lsort -unique $choices_test] $v_test] - #now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing - set chosen [lsearch -inline -nocase $allchoices $chosen] - set choice_in_list [expr {$chosen ne ""}] - } else { - set chosen $bestmatch - set choice_in_list 1 - } - } else { - set chosen [tcl::prefix::match -error "" [lsort -unique $allchoices] $c_check] - if {$chosen eq ""} { - set choice_in_list 0 - } else { - set choice_in_list 1 - } - } - #override choice_in_list if in deny list - #don't allow prefixing for elements from -choiceprefixdenylist - #we still use all elements to calculate the prefixes though - #review - case difference edge cases in choiceprefixdenylist !todo - if {$chosen in $choiceprefixdenylist} { - set choice_in_list 0 - set chosen "" - } - } - - #override the optimistic existing val - if {$choice_in_list && !$choice_exact_match} { - if {$choicemultiple_max != -1 && $choicemultiple_max < 2} { - if {$is_multiple} { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $idx $chosen - tcl::dict::set $dname $argname $existing - } else { - tcl::dict::set $dname $argname $chosen - } - } else { - if {$is_multiple} { - set existing_all [tcl::dict::get [set $dname] $argname] - lset existing_all $idx $choice_idx $chosen - tcl::dict::set $dname $argname $existing_all - } else { - set existing [tcl::dict::get [set $dname] $argname] - lset existing $choice_idx $chosen - tcl::dict::set $dname $argname $existing - } - } - } - } else { - #value as stored in $dname is ok - set choice_in_list [expr {$v_test in $choices_test}] - } - } - - if {!$choice_in_list && !$matches_default} { - if {!$choicerestricted} { - #if {$is_multiple} { - # set existing [tcl::dict::get [set $dname] $argname] - # lset existing $idx $v_test - # tcl::dict::set $dname $argname $existing - #} else { - # tcl::dict::set $dname $argname $v_test - #} - lappend vlist_validate $c - lappend vlist_check_validate $c_check - } else { - #unhappy path - if {$choiceprefix} { - set prefixmsg " (or a unique prefix of a value)" - } else { - set prefixmsg "" - } - #review: $c vs $c_check for -badval? - set msg "$argclass '$argname' for %caller% must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg - #arg_error "Option $argname for [Get_caller] must be one of the listed values:\n [join $allchoices "\n "]\n$casemsg$prefixmsg. Received: '$c'" $argspecs -badarg $argname - } - } - incr choice_idx - } - - incr idx - } - #reduce our vlist and vlist_check lists by removing choice matches as they need to be passed through without validation - #we also have retained any that match defaultval - whether or not it was in -choices or -choicegroups - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - if {[llength $vlist] && $has_default} { - set vlist_validate [list] - set vlist_check_validate [list] - foreach c $vlist c_check $vlist_check { - #for -choicemultiple with default that could be a list use 'ni' ?? review - if {$c_check ne $defaultval} { - lappend vlist_validate $c - lappend vlist_check_validate $c - } - } - set vlist $vlist_validate - set vlist_check $vlist_check_validate - } - - #is_allow_ansi doesn't apply to a value matching a supplied -default, or values matching those in -choices/-choicegroups - #assert: our vlist & vlist_check lists have been reduced to remove those - if {[llength $vlist] && !$is_allow_ansi} { - #allow_ansi 0 - package require punk::ansi - #do not run ta::detect on a list - foreach e $vlist { - if {[punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% contains ansi - but -allow_ansi is false. character-view: '[punk::ansi::ansistring VIEW $e]'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list contentviolation ansi] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - #puts "argname:$argname v:$v is_default:$is_default" - #we want defaults to pass through - even if they don't pass the checks that would be required for a specified value - #If the caller manually specified a value that happens to match the default - we don't detect that as any different from an unspecified value - Review. - #arguments that are at their default are not subject to type and other checks - - #don't validate defaults or choices that matched - #puts "---> opts_and_values: $opts_and_values" - #puts "===> argname: $argname is_default: $is_default is_choice: $is_choice" - #if {(!$has_choices && !$is_default) || ($has_choices && (!$is_default && !$choices_all_match))} {} - - #our validation-required list could have been reduced to none e.g if match -default or defined -choices/-choicegroups - #assert [llength $vlist] == [llength $vlist_check] - if {[llength $vlist]} { - switch -- $type { - literal { - foreach e $vlist { - if {$e ne $argname} { - set msg "$argclass '$argname' for %caller% requires literal value '$argname'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - any {} - list { - foreach e_check $vlist_check { - if {![tcl::string::is list -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'list'. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[llength $e_check] < $checkval} { - set msg "$argclass '$argname for %caller% requires list with -minsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[llength $e_check] > $checkval} { - set msg "$argclass '$argname for %caller% requires list with -maxsize $checkval. Received len:[llength $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $e_check -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - indexexpression { - foreach e_check $vlist_check { - if {[catch {lindex {} $e_check}]} { - set msg "$argclass $argname for %caller% requires type indexexpression. An index as used in Tcl list commands. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - string - ansistring - globstring { - #we may commonly want exceptions that ignore validation rules - most commonly probably the empty string - #we possibly don't want to always have to regex on things that don't pass the other more basic checks - # -regexprefail -regexprepass (short-circuiting fail/pass run before all other validations) - # -regexpostfail -regexpostpass (short-circuiting fail/pass run after other toplevel validations - but before the -validationtransform) - # in the comon case there should be no need for a tentative -regexprecheck - just use a -regexpostpass instead - # however - we may want to run -regexprecheck to restrict the values passed to the -validationtransform function - # -regexpostcheck is equivalent to -regexpostpass at the toplevel if there is no -validationtransform (or if it is in the -validationtransform) - # If there is a -validationtransform, then -regexpostcheck will either progress to run the -validationtransform if matched, else produce a fail - - #todo? - way to validate both unstripped and stripped? - set pass_quick_list_e [list] - set pass_quick_list_e_check [list] - set remaining_e $vlist - set remaining_e_check $vlist_check - #review - order of -regexprepass and -regexprefail in original rawargs significant? - #for now -regexprepass always takes precedence - if {$regexprepass ne ""} { - foreach e $vlist e_check $vlist_check { - if {[regexp $regexprepass $e]} { - lappend pass_quick_list_e $e - lappend pass_quick_list_e_check $e_check - } - } - set remaining_e [punklib_ldiff $vlist $pass_quick_list_e] - set remaining_e_check [punklib_ldiff $vlist_check $pass_quick_list_e_check] - } - if {$regexprefail ne ""} { - foreach e $remaining_e e_check $remaining_e_check { - #puts "----> checking $e vs regex $regexprefail" - if {[regexp $regexprefail $e]} { - if {[tcl::dict::exists $thisarg -regexprefailmsg]} { - #review - %caller% ?? - set msg [tcl::dict::get $thisarg -regexprefailmsg] - } else { - set msg "$argclass $argname for %caller% didn't pass regexprefail regex: '$regexprefail' got '$e'" - } - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list regexprefail $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - switch -- $type { - ansistring { - #we need to respect -validate_ansistripped for -minsize etc, but the string must contain ansi - #.. so we need to look at the original values in $vlist not $vlist_check - - #REVIEW - difference between string with mixed plaintext and ansi and one required to be ansicodes only?? - #The ansicodes only case should be covered by -minsize 0 -maxsize 0 combined with -validate_ansistripped ??? - package require punk::ansi - foreach e $remaining_e { - if {![punk::ansi::ta::detect $e]} { - set msg "$argclass '$argname' for %caller% requires ansistring - but no ansi detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - globstring { - foreach e $remaining_e { - if {![regexp {[*?\[\]]} $e]} { - set msg "$argclass '$argname' for %caller% requires globstring - but no glob characters detected" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $e -argspecs $argspecs]] $msg - } - } - } - } - - if {[tcl::dict::size $thisarg_checks]} { - foreach e_check $remaining_e_check { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::string::length $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -minsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::string::length $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires string with -maxsize $checkval. Received len:[tcl::string::length $e_check] value:'$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - number { - #review - consider effects of Nan and Inf - #NaN can be considered as 'technically' a number (or at least a special numeric value) - lassign {} low high ;#set both empty - set has_range 0 - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - set has_range 1 - } - } - foreach e $vlist e_check $vlist_check { - if {(![tcl::string::is integer -strict $e_check]) && (![tcl::string::is double -strict $e_check])} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$has_range} { - if {[::tcl::mathfunc::isnan $e]} { - set msg "$argclass '$argname' for %caller% must be an int or double within specified range {'$low' '$high'} NaN not comparable to any range. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {$low eq ""} { - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } elseif {$high eq ""} { - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be an int or double greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } else { - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be an int or double between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - int { - #-range can be expressed as two integers or an integer and an empty string e.g {0 ""} >= 0 or {"" 10} <=10 or {-1 10} -1 to 10 inclusive - if {[tcl::dict::exists $thisarg -range]} { - lassign [tcl::dict::get $thisarg -range] low high - if {"$low$high" ne ""} { - if {$low eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #lowside unspecified - check only high - if {$e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer less than or equal to $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } elseif {$high eq ""} { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #highside unspecified - check only low - if {$e_check < $low} { - set msg "$argclass '$argname' for %caller% must be integer greater than or equal to $low. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } else { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass '$argname' for %caller% requires type 'integer'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - #high and low specified - if {$e_check < $low || $e_check > $high} { - set msg "$argclass '$argname' for %caller% must be integer between $low and $high inclusive. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } else { - foreach e_check $vlist_check { - if {![tcl::string::is integer -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type integer. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - double { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is double -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type double. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - #safe jumptable test - #dict for {checkopt checkval} $thisarg_checks {} - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -range { - #todo - small-value double comparisons with error-margin? review - #todo - empty string for low or high - lassign $checkval low high - if {$e_check < $low || $e_check > $high} { - set msg "$argclass $argname for %caller% must be double between $low and $high. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list rangeviolation $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - bool { - foreach e_check $vlist_check { - if {![tcl::string::is boolean -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type boolean. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - dict { - foreach e_check $vlist_check { - if {[llength $e_check] %2 != 0} { - set msg "$argclass '$argname' for %caller% requires type 'dict' - must be key value pairs. Received: '$e_check'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - if {[tcl::dict::size $thisarg_checks]} { - tcl::dict::for {checkopt checkval} $thisarg_checks { - switch -- $checkopt { - -minsize { - # -1 for disable is as good as zero - if {[tcl::dict::size $e_check] < $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -minsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type minsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - -maxsize { - if {$checkval ne "-1"} { - if {[tcl::dict::size $e_check] > $checkval} { - set msg "$argclass '$argname' for %caller% requires dict with -maxsize $checkval. Received dict size:[dict size $e_check]" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type maxsize $checkval] -badarg $argname -badval $e_check -argspecs $argspecs]] $msg - } - } - } - } - } - } - } - } - alnum - - alpha - - ascii - - control - - digit - - graph - - lower - - print - - punct - - space - - upper - - wordchar - - xdigit { - foreach e $vlist e_check $vlist_check { - if {![tcl::string::is $type -strict $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - } - file - - directory - - existingfile - - existingdirectory { - foreach e $vlist e_check $vlist_check { - #//review - we may need '?' char on windows - if {!([tcl::string::length $e_check]>0 && ![regexp {[\"*<>\;]} $e_check])} { - #what about special file names e.g on windows NUL ? - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which doesn't look like it could be a file or directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -argspecs $argspecs]] $msg - } - } - if {$type eq "existingfile"} { - foreach e $vlist e_check $vlist_check { - if {![file exists $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing file" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } elseif {$type eq "existingdirectory"} { - foreach e $vlist e_check $vlist_check { - if {![file isdirectory $e_check]} { - set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e' which is not an existing directory" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - char { - #review - char vs unicode codepoint vs grapheme? - foreach e $vlist e_check $vlist_check { - if {[tcl::string::length $e_check] != 1} { - set msg "$argclass $argname for %caller% requires type 'character'. Received: '$e' which is not a single character" - return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $type] -badarg $argname -badval $e -argspecs $argspecs]] $msg - } - } - } - } - - } - - if {$is_strip_ansi} { - set stripped_list [lmap e $vlist_original {punk::ansi::ansistrip $e}] ;#no faster or slower, but more concise than foreach - if {[tcl::dict::get $thisarg -multiple]} { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict $argname $stripped_list - } - option { - tcl::dict::set opts $argname $stripped_list - } - value { - tcl::dict::set values_dict $argname $stripped_list - } - } - } else { - switch -- [tcl::dict::get $thisarg -ARGTYPE] { - leader { - tcl::dict::set leaders_dict [lindex $stripped_list 0] - } - option { - tcl::dict::set opts $argname [lindex $stripped_list 0] - } - value { - tcl::dict::set values_dict [lindex $stripped_list 0] - } - } - } - } - } - - #maintain order of opts $opts values $values as caller may use lassign. - set receivednames [list {*}$leadernames_received {*}$flagsreceived {*}$valnames_received] - if {[llength $receivednames]} { - #flat zip of names with overall posn, including opts - #set received_posns [concat {*}[lmap a $receivednames b [zero_based_posns [llength $receivednames]] {list $a $b}]] - set i -1 - set received_posns [concat {*}[lmap a $receivednames {list $a [incr i]}]] - } else { - set received_posns [list] - } - #Note that $received_posns is often tested as if a dict by functions to determine quickly if a variable was received (versus just having a default value) - #(e.g using 'dict exists $received -flag') - # - but it can have duplicate keys when args/opts have -multiple 1 - #It is actually a list of paired elements - return [tcl::dict::create leaders $leaders_dict opts $opts values $values_dict received $received_posns solos $solosreceived multis $multisreceived] - } - - - proc forms {id} { - set spec [get_spec $id] - if {[dict size $spec]} { - return [dict get $spec form_names] - } else { - return [list] - } - } - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis - @cmd -name punk::args::synopsis -help\ - "Return synopsis for each form of a command id - on separate lines. - If -form is given, supply only - the synopsis for that form. - " - @opts - -form -type string -default * - -return -type string -default full -choices {full summary dict} - @values -min 1 -max -1 - cmditem -multiple 1 -optional 0 - }] - proc synopsis {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis] - - if {[catch {package require punk::ansi} errM]} { - set has_punkansi 0 - } else { - set has_punkansi 1 - } - if {$has_punkansi} { - set I [punk::ansi::a+ italic] - set RST [punk::ansi::a] - } else { - set I "" - set RST "" - } - - ##set form * - ##if {[lindex $args 0] eq "-form"} { - ## set arglist [lrange $args 2 end] - ## set form [lindex $args 1] - ##} else { - ## set arglist $args - ##} - ##if {[llength $arglist] == 0} { - ## error "punk::args::synopsis expected command id possibly with trailing subcommands/args" - ##} - ##set id [lindex $arglist 0] - ##set cmdargs [lrange $arglist 1 end] - - lassign [dict values $argd] leaders opts values - set form [dict get $opts -form] - set opt_return [dict get $opts -return] - set cmditems [dict get $values cmditem] - set id [lindex $cmditems 0] - set cmdargs [lrange $cmditems 1 end] - - - set spec [get_spec $id] - if {$spec eq ""} { - return - } - set form_names [dict get $spec form_names] - if {$form ne "*"} { - if {[string is integer -strict $form]} { - set f [lindex $form_names $form] - if {$f ne ""} { - set form_names [list $f] - } else { - set form_names [list] - } - } else { - if {$form in $form_names} { - set form_names [list $form] - } else { - set form_names [list] - } - } - } - - set SYND [dict create] - set syn "" - #todo - -multiple etc - foreach f $form_names { - set SYNLIST [list] - dict set SYND $f [list] - append syn "$id" - set forminfo [dict get $spec FORMS $f] - foreach argname [dict get $forminfo LEADER_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class leader] - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display [lindex [dict get $arginfo -choices] 0] - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display $I$argname$RST - } - if {[dict get $arginfo -optional]} { - append syn " ?$display?" - } else { - append syn " $display" - } - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo OPT_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class option] - set tp [dict get $arginfo -type] - if {[dict get $arginfo -optional]} { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "?$argname?..." - } else { - set display "?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display "?$argname?" - } else { - set display "?$argname <$tp>?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - if {$tp eq "none"} { - set display "$argname ?$argname...?" - } else { - set display "$argname <$tp> ?$argname <$tp>?..." - } - } else { - if {$tp eq "none"} { - set display $argname - } else { - set display "$argname <$tp>" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - foreach argname [dict get $forminfo VAL_NAMES] { - set arginfo [dict get $forminfo ARG_INFO $argname] - set ARGD [dict create argname $argname class value] - if {[dict get $arginfo -optional] || [dict exists $arginfo -default]} { - if {[dict get $arginfo -multiple]} { - set display "?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "?[lindex [dict get $arginfo -choices] 0]?" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display "?$argname?" - } else { - set display "?$I$argname$RST?" - } - } - } else { - if {[dict get $arginfo -multiple]} { - set display "$I$argname$RST ?$I$argname$RST?..." - } else { - if {[dict exists $arginfo -choices] && [llength [dict get $arginfo -choices]] == 1} { - set display "[lindex [dict get $arginfo -choices] 0]" - } elseif {[dict get $arginfo -type] eq "literal"} { - set display $argname - } else { - set display "$I$argname$RST" - } - } - } - append syn " $display" - dict set ARGD type [dict get $arginfo -type] - dict set ARGD optional [dict get $arginfo -optional] - dict set ARGD display $display - dict lappend SYND $f $ARGD - } - append syn \n - } - switch -- $opt_return { - full { - return [string trim $syn \n] - } - summary { - set summary "" - dict for {form arglist} $SYND { - append summary $id - set class_state leader - set option_count 0 - set value_count 0 - foreach ainfo $arglist { - switch -- [dict get $ainfo class] { - leader { - append summary " [dict get $ainfo display]" - } - option { - incr option_count - } - value { - incr value_count - if {$class_state ne "value"} { - if {$option_count > 0} { - append summary " ?options ($option_count defined)?" - } - set class_state value - } - append summary " [dict get $ainfo display]" - } - } - } - if {$value_count == 0 && $option_count > 0} { - append summary " ?options ($option_count defined)?" - } - append summary \n - } - set summary [string trim $summary \n] - return $summary - } - dict { - return $SYND - } - } - } - - - lappend PUNKARGS [list { - @id -id ::punk::args::synopsis_summary - @cmd -name punk::args::synopsis_summary -help\ - "Reduce the width of a synopsis string - by coalescing options to ?options?... - synopsis string may be arbitrarily marked - up with ANSI codes." - @opts - @values -min 1 -max -1 - synopsis -multiple 0 -optional 0 - }] - proc synopsis_summary {args} { - set argd [punk::args::parse $args withid ::punk::args::synopsis_summary] - set synopsis [dict get $argd values synopsis] - set summary "" - foreach sline [split $synopsis \n] { - set sline [regsub -all {\s+} $sline " "] ;#normalize to single spacing only - review - set in_opt 0 - set line_out "" - set codestack [list] - set parts [punk::ansi::ta::split_codes_single $sline] - #basic - foreach {pt code} $parts { - set charlist [split $pt ""] - for {set i 0} {$i < [llength $charlist]} {incr i} { - set c [lindex $charlist $i] - - switch -- $c { - ? { - if {!$in_opt} { - set in_opt 1 - } else { - - } - } - " " { - if {!$in_opt} { - append line_out " " - } else { - set in_opt - } - } - default { - if {!$in_opt} { - append line_out $c - } - } - } - } - if {$code ne ""} { - if {[punk::ansi::codetype::is_sgr_reset $code]} { - set codestack [list "\x1b\[m"] - } elseif {[punk::ansi::codetype::has_sgr_leadingreset $code]} { - set codestack [list $code] - } elseif {[punk::ansi::codetype::is_sgr $code]} { - #basic simplification first - remove straight dupes - set dup_posns [lsearch -all -exact $codestack $code] ;#must be -exact because of square-bracket glob chars - set codestack [lremove $codestack {*}$dup_posns] - lappend codestack $code - } else { - #? ignore other ANSI codes? - } - } - } - if {[string match -* $plain_s] || [string match ?- $plain_s]} { - } - } - return $summary - } - - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args ---}] -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# Secondary API namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::lib { - variable PUNKARGS - tcl::namespace::export * - tcl::namespace::path [list [tcl::namespace::parent]] - #*** !doctools - #[subsection {Namespace punk::args::lib}] - #[para] Secondary functions that are part of the API - #[list_begin definitions] - - #proc utility1 {p1 args} { - # #*** !doctools - # #[call lib::[fun utility1] [arg p1] [opt {option value...}]] - # #[para]Description of utility1 - # return 1 - #} - - proc flatzip {l1 l2} { - concat {*}[lmap a $l1 b $l2 {list $a $b}] - } - - if {[info commands lseq] ne ""} { - #tcl 8.7+ lseq significantly faster, especially for larger ranges - #The internal rep can be an 'arithseries' with no string representation - proc zero_based_posns {count} { - if {$count < 1} {return} - lseq 0 $count-1 - } - } else { - proc zero_based_posns {count} { - if {$count < 1} {return} - lsearch -all [lrepeat $count 0] * - } - } - - #return list of single column-width marks - possibly with ansi - proc choiceinfo_marks {choice choiceinfodict} { - set marks [list] - if {[dict exists $choiceinfodict $choice]} { - set cinfo [dict get $choiceinfodict $choice] - foreach info $cinfo { - if {[lindex $info 0] eq "doctype"} { - switch -- [lindex $info 1] { - punkargs { - lappend marks [punk::ns::Cmark punkargs brightgreen] - } - ensemble { - lappend marks [punk::ns::Cmark ensemble brightyellow] - } - oo { - lappend marks [punk::ns::Cmark oo brightcyan] - } - ooc { - lappend marks [punk::ns::Cmark ooc cyan] - } - ooo { - lappend marks [punk::ns::Cmark ooo cyan] - } - native { - lappend marks [punk::ns::Cmark native] - } - unknown { - lappend marks [punk::ns::Cmark unknown brightred] - } - } - } - } - } - return $marks - } - - - #experiment with equiv of js template literals with ${expression} in templates - #e.g tstr {This is the value of x in calling scope ${$x} !} - #e.g tstr -allowcommands {This is the value of x in calling scope ${[set x]} !} - #e.g tstr -allowcommands {This is the value of [lindex $x -1] in calling scope ${[lindex [set x] 0]} !} - lappend PUNKARGS [list { - @id -id ::punk::args::lib::tstr - @cmd -name punk::args::lib::tstr -help\ - "A rough equivalent of js template literals - - Substitutions: - \$\{$varName\} - \$\{[myCommand]\} - (when -allowcommands flag is given)" - -allowcommands -default 0 -type none -help\ - "If -allowcommands is present, placeholder can contain commands - e.g {\$\{plaintext0 [lindex $var 0] plaintext2\}}" - -undent -default 1 -type boolean -help\ - "undent/dedent the template lines. - The longest common prefix of whitespace is removed" - -indent -default "" -type string -help\ - "String with which to indent the template - prior to substitution. - If -undent is enabled, that is performed - first, then the indent is applied." - -paramindents -default line -choices {none line position} -choicelabels { - line\ - " Use leading whitespace in - the line in which the - placeholder occurs." - position\ - " Use the position in - the line in which the - placeholder occurs." - none\ - " No indents applied to - subsequent placeholder value - lines. This will usually - result in text awkwardly - ragged unless the source code - has also been aligned with the - left margin or the value has - been manually padded." - } -help\ - "How indenting is done for subsequent lines in a - multi-line placeholder substitution value. - The 1st line or a single line value is always - placed at the placeholder. - paramindents are performed after the main - template has been indented/undented. - (indenting by position does not calculate - unicode double-wide or grapheme cluster widths) - " - #choicelabels indented by 1 char is clearer for -return string - and reasonable in table - -return -default string -choices {dict list string args}\ - -choicelabels { - dict\ - " Return a dict with keys - 'template', 'params' and - 'errors'" - string\ - " Return a single result - being the string with - placeholders substituted." - list\ - " Return a 2 element list. - The first is itself a list - of plaintext portions of the - template, split at each point - at which placeholders were - present. The second element - of the outer list is a list - of placeholder values if -eval - is 1, or a list of the raw - placeholder strings if -eval - is 0." - args\ - " Return a list where the first - element is a list of template - plaintext sections as per the - 'list' return mechanism, but the - placeholder items are individual - items in the returned list. - This can be useful when passing - the expanded result of a tstr - command to another function - which expects the placeholders - as individual arguments" - } - -eval -default 1 -type boolean -help\ - "Whether to evaluate the \$\{\} placeholders. - When -return is string, -eval should generally be set to 1. - For a string return, -eval 0 will result in the raw contents of \$\{\} being substituted. - contained variables in that case should be braced or whitespace separated, or the variable - name is likely to collide with surrounding text. - e.g tstr -return string -eval 0 {plaintext\$\{\$\{var\}\}plaintext} -> plaintext\$\{var\}plaintext" - @values -min 0 -max 1 - templatestring -help\ - "This argument should be a braced string containing placeholders such as \$\{$var\} e.g {The value is \$\{$var\}.} - where $var will be substituted from the calling context - The placeholder itself can contain plaintext portions as well as variables. - It can contain commands in square brackets if -allowcommands is true - e.g tstr -return string -allowcommands {Tcl Version:\$\{[info patch]\} etc} - - Escape sequences such as \\n and unicode escapes are processed within placeholders. - " - }] - - proc tstr {args} { - #Too hard to fully eat-our-own-dogfood from within punk::args package - # - we use punk::args within the unhappy path only - #set argd [punk::args::get_by_id ::punk::lib::tstr $args] - #set templatestring [dict get $argd values templatestring] - #set opt_allowcommands [dict get $argd opts -allowcommands] - #set opt_return [dict get $argd opts -return] - #set opt_eval [dict get $argd opts -eval] - - set templatestring [lindex $args end] - set arglist [lrange $args 0 end-1] - set opts [dict create\ - -allowcommands 0\ - -undent 1\ - -indent ""\ - -paramindents line\ - -eval 1\ - -return string\ - ] - if {"-allowcommands" in $arglist} { - set arglist [::punk::args::system::punklib_ldiff $arglist -allowcommands] - dict set opts -allowcommands 1 - } - if {[llength $arglist] % 2 != 0} { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr expected option/value pairs prior to last argument" - } - } - dict for {k v} $arglist { - set fullk [tcl::prefix::match -error "" {-allowcommands -indent -undent -paramindents -return -eval} $k] - switch -- $fullk { - -indent - -undent - -paramindents - -return - -eval { - dict set opts $fullk $v - } - default { - if {[info commands ::punk::args::get_by_id] ne ""} { - punk::args::get_by_id ::punk::args::lib::tstr $args - return - } else { - error "punk::args::lib::tstr unknown option $k. Known options: [dict keys $opts]" - } - } - } - } - set opt_allowcommands [dict get $opts -allowcommands] - set opt_paramindents [dict get $opts -paramindents] - set test_paramindents [tcl::prefix::match -error "" {none line position} $opt_paramindents] - if {$test_paramindents ni {none line position}} { - error "punk::args::lib::tstr option -paramindents invalid value '$opt_paramindents'. Must be one of none, line, position or an unambiguous abbreviation thereof." - } - set opt_paramindents $test_paramindents - set opt_return [dict get $opts -return] - set opt_return [tcl::prefix::match -error "" {args dict list string} $opt_return] - if {$opt_return eq ""} { - } - set opt_eval [dict get $opts -eval] - - - set nocommands "-nocommands" - if {$opt_allowcommands == 1} { - set nocommands "" - } - - set opt_undent [dict get $opts -undent] - if {$opt_undent} { - set templatestring [punk::args::lib::undent $templatestring] - } - set opt_indent [dict get $opts -indent] - if {$opt_indent ne ""} { - set templatestring [punk::args::lib::indent $templatestring $opt_indent] - } - - #set parts [_tstr_split $templatestring] - if {[string first \$\{ $templatestring] < 0} { - set parts [list $templatestring] - } else { - set parts [_parse_tstr_parts $templatestring] - } - set textchunks [list] - #set expressions [list] - set params [list] - set idx 0 - set errors [dict create] - set lastline "" ;#todo - first line has placeholder? - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach {pt expression} $parts { - lappend textchunks $pt - incr idx ;#pt incr - - #ignore last expression - if {$idx == [llength $parts]} { - break - } - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - #lappend expressions $expression - #---------------------- - #REVIEW - JMN - #TODO - debug punk::args loading of @dynamic defs - #puts "-- $expression" - #---------------------- - #brk1 - literal newline not {\n} - set leader "" - if {[set brk1 [string first \n $expression]] >= 0} { - #undent left of paramstart only for lines of expression that arent on opening ${..} line - set tail [string range $expression $brk1+1 end] - set leader [string repeat " " [string length $lastline]] - set undentedtail [punk::args::lib::undentleader $tail $leader] - #set undentedtail [punk::lib::undent [string range $expression $brk1+1 end]] - set expression "[string range $expression 0 $brk1]$undentedtail" - } - if {$opt_eval} { - if {[catch [list uplevel 1 [list ::subst {*}$nocommands $expression]] result]} { - lappend params [string cat \$\{ $expression \}] - dict set errors [expr {[llength $params]-1}] $result - } else { - set result [string map [list \n "\n$leader"] $result] - lappend params $result - } - #lappend params [uplevel 1 [list ::subst {*}$nocommands $expression]] - } else { - lappend params [subst -nocommands -novariables $expression] - } - append lastline [lindex $params end] ;#for current expression's position calc - - incr idx ;#expression incr - } - - if {$opt_return eq "dict"} { - return [dict create template $textchunks params $params errors $errors] - } - if {[dict size $errors]} { - set einfo "" - dict for {i e} $errors { - append einfo "parameter $i error: $e" \n - } - #REVIEW!!! - #TODO - fix - #puts stderr "tstr errors:\n$einfo\n" - } - - switch -- $opt_return { - list { - return [list $textchunks $params] - } - args { - #see example in tstr_test_one - return [list $textchunks {*}$params] - } - string { - #todo - flag to disable indent-matching behaviour for multiline param? - set out "" - set pt1 [lindex $parts 0] - set lastline_posn [string last \n $pt1] - if {$lastline_posn >= 0} { - set lastline [string range $pt1 $lastline_posn+1 end] - } else { - set lastline $pt1 - } - foreach pt $textchunks param $params { - if {$opt_paramindents eq "none"} { - append out $pt $param - } else { - set lastline_posn [string last \n $pt] - if {$lastline_posn >= 0} { - set lastline [string range $pt $lastline_posn+1 end] - } - if {$opt_paramindents eq "line"} { - regexp {(\s*).*} $lastline _all lastindent - } else { - #position - #TODO - detect if there are grapheme clusters - #This regsub doesn't properly space unicode double-wide chars or clusters - set lastindent "[regsub -all {\S} $lastline " "] " - } - if {$lastindent ne ""} { - set paramlines [split $param \n] - if {[llength $paramlines] == 1} { - append out $pt $param - } else { - append out $pt [lindex $paramlines 0] - foreach nextline [lrange $paramlines 1 end] { - append out \n $lastindent $nextline - } - } - } else { - append out $pt $param - } - append lastline $param - } - } - return $out - } - } - } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { - set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. - example: - set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] - } - - @values -min 2 -max 2 - template -type list -minsize 2 -maxsize 2 -help "This could be supplied directly as a 2 element list of each half of the sql statement - - but the tstr call in the example does this for you, and also passes in the id automatically" - - where -type int -help {Integer param for where clause. tstr mechanism above will pass the id as the second parameter} - }] - set template [dict get $argd values template] - set where [dict get $argd values where] - #set result [join [list [lindex $template 0] $where [lindex $template 1]] ""] - set result [string cat [lindex $template 0] $where [lindex $template 1]] - return $result - } - proc _parse_tstr_parts {templatestring} { - if {$templatestring eq ""} { - return [list] - } - set chars [split $templatestring ""] - set in_placeholder 0 - set tchars "" - set echars "" - set parts [list] - set i 0 - foreach ch $chars { - if {!$in_placeholder} { - set nextch [lindex $chars [expr {$i+1}]] - if {"$ch$nextch" eq "\$\{"} { - set in_placeholder 2 ;#2 to signify we just entered placeholder - lappend parts $tchars - set tchars "" - } else { - append tchars $ch - } - } else { - if {$ch eq "\}"} { - if {[tcl::info::complete $echars]} { - set in_placeholder 0 - lappend parts $echars - set echars "" - } else { - append echars $ch - } - } else { - if {$in_placeholder == 2} { - #skip opening bracket dollar sign - set in_placeholder 1 - } else { - append echars $ch - } - } - } - incr i - } - if {$tchars ne ""} { - lappend parts $tchars - } - if {[llength $parts] % 2 == 0} { - #always trail with pt for consistency with _perlish_split method so we can test other mechanisms with odd-length pt/code../pt style list - lappend parts "" - } - return $parts - } - #based on punk::ansi::ta::_perlish_split - proc _tstr_split {text} { - if {$text eq ""} { - return {} - } - set list [list] - set start 0 - #ideally re should allow curlies within but we will probably need a custom parser to do it - #(js allows nested string interpolation) - #set re {\$\{[^\}]*\}} - set re {\$\{(?:(?!\$\{).)*\}} - - #eg regexp {\x1b(?:\(0(?:(?:(?!\x1b\(B).)*\x1b\(B)|\)0(?:(?:(?!\x1b\)B).)*\x1b\)B))} $code - - #We can get $matchEnd < $matchStart; we need to ensure there is an exit condition for non-greedy empty results REVIEW - while {[regexp -start $start -indices -- $re $text match]} { - lassign $match matchStart matchEnd - #puts "->start $start ->match $matchStart $matchEnd" - if {$matchEnd < $matchStart} { - puts "e:$matchEnd < s:$matchStart" - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::index $text $matchStart] - incr start - if {$start >= [tcl::string::length $text]} { - break - } - continue - } - lappend list [tcl::string::range $text $start $matchStart-1] [tcl::string::range $text $matchStart+2 $matchEnd-1] - set start [expr {$matchEnd+1}] - #? - if {$start >= [tcl::string::length $text]} { - break - } - } - return [lappend list [tcl::string::range $text $start end]] - } - - #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. - proc indent {text {prefix " "}} { - set result [list] - foreach line [split $text \n] { - if {[string trim $line] eq ""} { - lappend result "" - } else { - lappend result $prefix[string trimright $line] - } - } - return [join $result \n] - } - #dedent? - proc undent {text} { - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - regexp {^([\t ]*)} $lcp _m lcp - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - - #hacky - proc undentleader {text leader} { - #leader usually whitespace - but doesn't have to be - if {$text eq ""} { - return "" - } - set lines [split $text \n] - set nonblank [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - continue - } - lappend nonblank $ln - } - lappend nonblank "${leader}!!" - set lcp [longestCommonPrefix $nonblank] - if {$lcp eq ""} { - return $text - } - #regexp {^([\t ]*)} $lcp _m lcp - #lcp can be shorter than leader - set lcp [string range $lcp 0 [string length $leader]-1] - - if {$lcp eq ""} { - return $text - } - set len [string length $lcp] - set result [list] - foreach ln $lines { - if {[string trim $ln] eq ""} { - lappend result "" - } else { - lappend result [string range $ln $len end] - } - } - return [join $result \n] - } - #A version of textutil::string::longestCommonPrefixList - proc longestCommonPrefix {items} { - if {[llength $items] <= 1} { - return [lindex $items 0] - } - set items [lsort $items[unset items]] - set min [lindex $items 0] - set max [lindex $items end] - #if first and last of sorted list share a prefix - then all do (first and last of sorted list are the most different in the list) - #(sort order nothing to do with length - e.g min may be longer than max) - if {[string length $min] > [string length $max]} { - set temp $min - set min $max - set max $temp - } - set n [string length $min] - set prefix "" - set i -1 - while {[incr i] < $n && ([set c [string index $min $i]] eq [string index $max $i])} { - append prefix $c - } - return $prefix - } - - - #*** !doctools - #[list_end] [comment {--- end definitions namespace punk::args::lib ---}] -} - -tcl::namespace::eval punk::args::argdocbase { - namespace export * - #use a? to test and create literal ansi here rather than relying on punk::ansi package presence - #e.g - #% a? bold - #- bold │SGR 1│sample│␛[1msample - #- ──────┼─────┼──────┼────────── - #- RESULT│ │sample│␛[1msample - proc B {} {return \x1b\[1m} ;#a+ bold - proc N {} {return \x1b\[22m} ;#a+ normal - proc I {} {return \x1b\[3m} ;#a+ italic - proc NI {} {return \x1b\[23m} ;#a+ noitalic - -} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -tcl::namespace::eval punk::args::package { - variable PUNKARGS - lappend PUNKARGS [list { - @dynamic - @id -id "::punk::args::package::standard_about" - @cmd -name "%pkg%::about" -help\ - "About %pkg% - ... - " - -package_about_namespace -type string -optional 0 -help\ - "Namespace containing the package about procedures - Must contain " - -return\ - -type string\ - -default table\ - -choices {string table tableobject}\ - -choicelabels { - string\ - "A basic text layout" - table\ - "layout in table borders - (requires package: textblock)" - tableobject\ - "textblock::class::table object instance" - }\ - -help\ - "Choose the return type of the 'about' information" - topic -optional 1\ - -nocase 1\ - -default {*}\ - -choices {Description License Version Contact *}\ - -choicerestricted 0\ - -choicelabels { - - }\ - -multiple 1\ - -help\ - "Topic to display. Omit or specify as * to see all. - If * is included with explicit topics, * represents - the remaining unmentioned topics." - }] - proc standard_about {args} { - set argd [punk::args::parse $args withid ::punk::args::package::standard_about] - lassign [dict values $argd] leaders OPTS values received - - set pkgns [dict get $OPTS -package_about_namespace] - if {[info commands ${pkgns}::package_name] eq ""} { - error "punk::args::package::standard_about unable to find function ${pkgns}::package_name" - } - set pkgname [${pkgns}::package_name] - - set opt_return [dict get $OPTS -return] - set all_topics [${pkgns}::about_topics] - if {![dict exists $received topic]} { - set topics $all_topics - } else { - # * represents all remaining topics not explicitly mentioned. - set val_topics [dict get $values topic] ;#if -multiple is true, this is a list - set explicit_topics [lsearch -all -inline -exact -not $val_topics "*"] - set topics [list] - foreach t $val_topics { - if {$t eq "*"} { - foreach a $all_topics { - if {$a ni $explicit_topics} { - lappend topics $a - } - } - } else { - lappend topics $t - } - } - } - if {$opt_return ne "string"} { - package require textblock ;#table support - set is_table 1 - set title [string cat {[} $pkgname {]} ] - set t [textblock::class::table new -title $title] - $t configure -frametype double -minwidth [expr {[string length $title]+2}] - - } else { - set topiclens [lmap t $topics {string length $t}] - set widest_topic [tcl::mathfunc::max {*}$topiclens] - set is_table 0 - set about "$pkgname\n" - append about [string repeat - $widest_topic] \n - } - foreach topic $topics { - if {[llength [info commands ::${pkgns}::get_topic_$topic]] == 1} { - set topic_contents [::${pkgns}::get_topic_$topic] - } else { - set topic_contents "" - } - if {!$is_table} { - set content_lines [split $topic_contents \n] - append about [format %-${widest_topic}s $topic] " " [lindex $content_lines 0] \n - foreach ln [lrange $content_lines 1 end] { - append about [format %-${widest_topic}s ""] " " $ln \n - } - } else { - $t add_row [list $topic $topic_contents] - } - } - - if {!$is_table} { - return $about - } else { - if {$opt_return eq "tableobject"} { - return $t - } - set result [$t print] - $t destroy - return $result - } - } - -} - -#usually we would directly call arg definitions near the defining proc, -# so that the proc could directly use the definition in its parsing. -# -#for punk::args we need to make sure the punk::args namespace is fully loaded before calling, so we do it at the end. -#arguably it may be more processor-cache-efficient to do together like this anyway. - -#can't do this here? - as there is circular dependency with punk::lib -#tcl::namespace::eval punk::args { -# foreach deflist $PUNKARGS { -# punk::args::define {*}$deflist -# } -# set PUNKARGS "" -#} - -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#*** !doctools -#[section Internal] -tcl::namespace::eval punk::args::system { - #*** !doctools - #[subsection {Namespace punk::args::system}] - #[para] Internal functions that are not part of the API - - #dict get value with default wrapper for tcl 8.6 - if {[info commands ::tcl::dict::getdef] eq ""} { - proc Dict_getdef {dictValue args} { - set keys [lrange $args 0 end-1] - if {[tcl::dict::exists $dictValue {*}$keys]} { - return [tcl::dict::get $dictValue {*}$keys] - } else { - return [lindex $args end] - } - } - } else { - #we pay a minor perf penalty for the wrap - interp alias "" ::punk::args::system::Dict_getdef "" ::tcl::dict::getdef - } - - #name to reflect maintenance - home is punk::lib::ldiff - proc punklib_ldiff {fromlist removeitems} { - if {[llength $removeitems] == 0} {return $fromlist} - set result {} - foreach item $fromlist { - if {$item ni $removeitems} { - lappend result $item - } - } - return $result - } - -} - - -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -## Ready -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.1.7 -}] -return - -#*** !doctools -#[manpage_end] - diff --git a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm index 5cfe3710..88b91288 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/libunknown-0.1.tm @@ -860,8 +860,9 @@ tcl::namespace::eval punk::libunknown { #todo - find the iscript in the '$epoch pkg epochs added paths' lists and determine os vs dev vs internal #(scanning for path directly in the ifneeded script for pkgs is potentially error prone) #for .tm ifneeded scripts - the syntax is simple enough to determine directly (and ifneeded scr not stored for those anyway) - if {[package ifneeded $pkg $v] ne $iscript} { - #puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath" + set justaddedscript [package ifneeded $pkg $v] + if {$justaddedscript ne $iscript} { + puts "---->refreshing $pkg $v - reverting to already stored from path:$searchpath versions: $versions" package ifneeded $pkg $v $iscript #dict set pkgvdone $pkg $v 1 } @@ -887,10 +888,10 @@ tcl::namespace::eval punk::libunknown { set prev_e [dict get $epoch pkg current] set current_e [expr {$prev_e + 1}] # ------------- - #puts stderr "--> pkg epoch $prev_e -> $current_e" - #puts stderr "args: $args" - #puts stderr "last_auto: $last_auto_path" - #puts stderr "auto_path: $auto_path" + puts stderr "--> pkg epoch $prev_e -> $current_e" + puts stderr "args: $args" + puts stderr "last_auto: $last_auto_path" + puts stderr "auto_path: $auto_path" # ------------- if {[llength $auto_path] > [llength $last_auto_path] && [punk::libunknown::lib::is_list_all_in_list $last_auto_path $auto_path]} { #The auto_path changed, and is a pure addition of entry/entries @@ -1108,7 +1109,7 @@ tcl::namespace::eval punk::libunknown { if {[string match ::* $pkg]} { error "packagedb_indexinfo: package name required - not a fully qualified namespace beginning with :: Received:'$pkg'" } - set versions [package versions $pkg] + set versions [lsort -command {package vcompare} [package versions $pkg]] if {[llength $versions] == 0} { set v [package provide $pkg] } @@ -1519,9 +1520,25 @@ tcl::namespace::eval punk::libunknown { set pkg_row $added set tm_epoch [dict get $epoch tm current] - set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + #set tm_added [punk::lib::showdict [dict get $epoch tm epochs $tm_epoch added] */$pkgname] + set added [dict get $epoch tm epochs $tm_epoch added] + set rows [list] + dict for {path pkgs} $added { + set c1 $path + set c2 [dict size $pkgs] + set c3 "" + if {[dict exists $pkgs $pkgname]} { + set vdict [dict get $pkgs $pkgname] + dict for {v data} $vdict { + append c3 "$v $data" \n + } + } + set r [list $c1 $c2 $c3] + lappend rows $r + } set title "TM epoch $tm_epoch - added" - set added [textblock::frame -title $title $tm_added] + #set added [textblock::frame -title $title $tm_added] + set added [textblock::table -title $title -headers [list Path Tmcount $pkgname] -rows $rows] set tm_row $added diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/loadedlib-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/loadedlib-0.1.0.tm index 5e12b9a2..3fb1e001 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/loadedlib-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/loadedlib-0.1.0.tm @@ -53,11 +53,6 @@ namespace eval punk::mix::commandset::loadedlib { #REVIEW - this doesn't result in full scans catch {package require frobznodule666} ;#ensure pkg system has loaded/searched for everything - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } set packages [package names] set matches [list] foreach search $searchstrings { @@ -85,11 +80,7 @@ namespace eval punk::mix::commandset::loadedlib { # set versions $v #} } - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {$opt_highlight} { set v [package provide $m] if {$v ne ""} { @@ -188,11 +179,6 @@ namespace eval punk::mix::commandset::loadedlib { } proc info {libname} { - if {[catch {package require natsort}]} { - set has_natsort 0 - } else { - set has_natsort 1 - } catch {package require $libname 1-0} ;#ensure pkg system has loaded/searched - using unsatisfiable version range set pkgsknown [package names] if {[set posn [lsearch $pkgsknown $libname]] >= 0} { @@ -201,11 +187,7 @@ namespace eval punk::mix::commandset::loadedlib { puts stderr "Package not found as available library/module - check tcl::tm::list and \$auto_path" } set versions [package versions [lindex $libname 0]] - if {$has_natsort} { - set versions [natsort::sort $versions] - } else { - set versions [lsort $versions] - } + set versions [lsort -command {package vcompare} $versions] if {![llength $versions]} { puts stderr "No version numbers found for library/module $libname" return false diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm index f670c8c0..8abe694e 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/project-0.1.0.tm @@ -592,10 +592,23 @@ namespace eval punk::mix::commandset::project { namespace export * namespace path [namespace parent] + punk::args::define { + @id -id ::punk::mix::commandset::project::collection::_default + @cmd -name "punk::mix::commandset::project::collection::_default"\ + -summary\ + "List projects under fossil managment."\ + -help\ + "List projects under fossil management, showing fossil db location and number of checkouts" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * + } #e.g imported as 'projects' - proc _default {{glob {}} args} { + proc _default {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::collection::_default] + set globlist [dict get $argd values glob] + #*** !doctools - #[call [fun _default] [arg glob] [opt {option value...}]] + #[call [fun _default] [arg glob...]] #[para]List projects under fossil management, showing fossil db location and number of checkouts #[para]The glob argument is optional unless option/value pairs are also supplied, in which case * should be explicitly supplied #[para]glob restricts output based on the name of the fossil db file e.g s* for all projects beginning with s @@ -604,7 +617,7 @@ namespace eval punk::mix::commandset::project { #[para] punk::overlay::import_commandset projects . ::punk::mix::commandset::project::collection #[para]Will result in the command being available as projects package require overtype - set db_projects [lib::get_projects $glob] + set db_projects [lib::get_projects {*}$globlist] set col1items [lsearch -all -inline -index 0 -subindices $db_projects *] set col2items [lsearch -all -inline -index 1 -subindices $db_projects *] set checkouts [lsearch -all -inline -index 2 -subindices $db_projects *] @@ -1012,12 +1025,21 @@ namespace eval punk::mix::commandset::project { #consider using punk::cap to enable multiple template-substitution providers with their own set of tagnames and/or tag wrappers, where substitution providers are all run return [string cat % $tagname %] } - #get project info only by opening the central confg-db - #(will not have proper project-name etc) - proc get_projects {{globlist {}} args} { - if {![llength $globlist]} { - set globlist [list *] - } + punk::args::define { + @id -id ::punk::mix::commandset::project::lib::get_projects + @cmd -name punk::mix::commandset::project::lib::get_projects\ + -summary\ + "List projects referred to by central fossil config-db."\ + -help\ + "Get project info only by opening the central fossil config-db + (will not have proper project-name etc)" + @values -min 0 -max -1 + glob -type string -multiple 1 -default * -optional 1 + } + proc get_projects {args} { + set argd [punk::args::parse $args withid ::punk::mix::commandset::project::lib::get_projects] + set globlist [dict get $argd values glob] + set fossil_prog [auto_execok fossil] set configdb [punk::repo::fossil_get_configdb] diff --git a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm index f1d51f9a..b57453f1 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/repl-0.1.2.tm @@ -3362,7 +3362,7 @@ namespace eval repl { #work around bug in safe base which won't load Tcl libs that have deeper nesting #(also affects tcllib page/plugins folder) - set termversions [package versions term] + set termversions [lsort -command {package vcompare} [package versions term]] set termv [lindex $termversions end] if {$termv ne ""} { set path [lindex [package ifneeded term $termv] end] ;#assuming path at end of something like "source .../term.tcl" diff --git a/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm index a4113c45..50bcc2f8 100644 --- a/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punkcheck-0.1.0.tm @@ -243,14 +243,10 @@ namespace eval punkcheck { } method get_targets_exist {} { set punkcheck_folder [file dirname [$o_installer get_checkfile]] + #puts stdout "### punkcheck glob -dir $punkcheck_folder -tails {*}$o_targets" + #targets can be paths such as punk/mix/commandset/module-0.1.0.tm - glob can search levels below supplied -dir set existing [glob -nocomplain -dir $punkcheck_folder -tails {*}$o_targets] - #set existing [list] - #foreach t $o_targets { - # if {[file exists [file join $punkcheck_folder $t]]} { - # lappend existing $t - # } - #} return $existing } method end {} { diff --git a/src/vfs/punk8bsd.vfs/main.tcl#..+_config+punk_main.tcl#@punk%3a%3aboot,merge_over#.fxlnk b/src/vfs/punk8bsd.vfs/main.tcl#..+_config+punk_main.tcl#@punk%3a%3aboot,merge_over#.fxlnk new file mode 100644 index 00000000..e69de29b