# -*- 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: punkshell/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::pipe 999999.0a1.0 # Meta platform tcl # Meta license MIT # @@ Meta End # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[manpage_begin punkshell_module_punk::pipe 0 999999.0a1.0] #[copyright "2025"] #[titledesc {Module API}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] #[require punk::pipe] #[keywords module] #[description] #[para] - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section Overview] #[para] overview of punk::pipe #[subsection Concepts] #[para] - # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Requirements # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[subsection dependencies] #[para] packages used by punk::pipe #[list_begin itemized] package require Tcl 8.6- #*** !doctools #[item] [package {Tcl 8.6}] # #package require frobz # #*** !doctools # #[item] [package {frobz}] #*** !doctools #[list_end] # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section API] tcl::namespace::eval punk::pipe { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Base namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[subsection {Namespace punk::pipe}] #[para] Core API functions for punk::pipe #[list_begin definitions] #proc sample1 {p1 n args} { # #*** !doctools # #[call [fun sample1] [arg p1] [arg n] [opt {option value...}]] # #[para]Description of sample1 # #[para] Arguments: # # [list_begin arguments] # # [arg_def tring p1] A description of string argument p1. # # [arg_def integer n] A description of integer argument n. # # [list_end] # return "ok" #} #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 should 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}] } #debatable whether boolean_almost_equal is more surprising than 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. #alternatively - 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)} } #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)} } proc val [list [list v [lreplace x 0 0]]] {return $v} #*** !doctools #[list_end] [comment {--- end definitions namespace punk::pipe ---}] } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # Secondary API namespace # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::pipe::lib { tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase tcl::namespace::path [tcl::namespace::parent] #*** !doctools #[subsection {Namespace punk::pipe::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 #} #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 [string trimleft $rhs] #--- #REVIEW! #set rhs [regsub -all {\s{1,}} $rhs {}] ;#collapse all internal whitespace to a single token #This stops us matching {/@**@x x} vs {/@**@x x} #--- set rhs [tcl::string::map {: ? * [ ] \\ {"} " " } $rhs] #review - we don't expect other command-incompatible chars such as colon? return $rhs } # 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 } } #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 } #note - empty data after trailing , is ignored. (comma as very last character) # - fix by documentation only. double up trailing comma e.g ,, if desired to return pattern match plus all at end! #todo - move to punk::pipe 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 token "" set end_var_posn -1 ;#first var_terminal encountered within each comma delimited section set token_index 0 ;#index of terminal char within each token set indq 0 set inbraces 0 set inesc 0 ;#whether last char was backslash (see also punk::escv) set prevc "" set char_index 0 #if {[string index $varspecs end] eq ","} { # set varspecs [string range $varspecs 0 end-1] #} set charcount 0 foreach c [split $varspecs ""] { incr charcount if {$indq} { if {$inesc} { #puts stderr "inesc adding '$c'" append token \\$c } else { if {$c eq {"}} { set indq 0 } else { append token $c } } } elseif {$inbraces} { if {$inesc} { append token \\$c } else { if {$c eq "\}"} { incr inbraces -1 if {$inbraces} { append token $c } } elseif {$c eq "\{"} { incr inbraces if {$inbraces} { append token $c } } 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 {"}} { if {!$inesc} { set indq 1 } else { append token $c } } elseif {$c eq "\{"} { if {!$inesc} { set inbraces 1 } else { append token $c } } elseif {$c eq ","} { #set var $token #set spec "" #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 # 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 { # if {$end_var_posn == 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 end_var_posn -1 } else { append token $c switch -exact -- $c { ' { set in_atom 1 } ( { incr in_brackets } default { if {$end_var_posn == -1 && (($c in $var_terminals) && ($prevc ni $protect_terminals))} { set end_var_posn $token_index } } } } if {$c eq ","} { set var $token set spec "" 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 { if {$end_var_posn == 0} { set var "" set spec $token } } lappend varlist [list [string trim $var] $spec] set token "" set token_index -1 set end_var_posn -1 } } if {$charcount == [string length $varspecs]} { if {!($indq || $inbraces || $in_atom || $in_brackets)} { if {$c ne ","} { set var $token set spec "" 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 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 { if {$end_var_posn == 0} { set var "" set spec $token } } lappend varlist [list [string trim $var] $spec] set token "" set token_index -1 set end_var_posn -1 } } } 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 [punk::lib::string_splitbefore $token $end_var_posn] # set var $token # set spec "" # if {$end_var_posn > 0} { # #lassign [scan $token %${end_var_posn}s%s] 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 { # if {$end_var_posn == 0} { # set var "" # set spec $token # } # } # #lappend varlist [list [string trim $var] [string trim $spec]] # #spec needs to be able to match whitespace too # lappend varlist [list [string trim $var] $spec] #} return $varlist } #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 [_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 _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] } #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] #JMN #debug.punk.pipe.compile {proc $cmdname} return $result } #*** !doctools #[list_end] [comment {--- end definitions namespace punk::pipe::lib ---}] } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ # == === === === === === === === === === === === === === === # Sample 'about' function with punk::args documentation # == === === === === === === === === === === === === === === tcl::namespace::eval punk::pipe { tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase variable PUNKARGS variable PUNKARGS_aliases lappend PUNKARGS [list { @id -id "(package)punk::pipe" @package -name "punk::pipe" -help\ "Package Description" }] namespace eval argdoc { #namespace for custom argument documentation proc package_name {} { return punk::pipe } proc about_topics {} { #info commands results are returned in an arbitrary order (like array keys) set topic_funs [info commands [namespace current]::get_topic_*] set about_topics [list] foreach f $topic_funs { set tail [namespace tail $f] lappend about_topics [string range $tail [string length get_topic_] end] } return $about_topics } proc default_topics {} {return [list Description outline *]} # ------------------------------------------------------------- # get_topic_ functions add more to auto-include in about topics # ------------------------------------------------------------- proc get_topic_Description {} { punk::args::lib::tstr [string trim { punk pipeline features } \n] } proc get_topic_License {} { return "MIT" } proc get_topic_Version {} { return $::punk::pipe::version } proc get_topic_Contributors {} { set authors {{Julian Noble }} set contributors "" foreach a $authors { append contributors $a \n } if {[string index $contributors end] eq "\n"} { set contributors [string range $contributors 0 end-1] } return $contributors } proc get_topic_outline {} { punk::args::lib::tstr -return string { todo.. } } # ------------------------------------------------------------- } # we re-use the argument definition from punk::args::standard_about and override some items set overrides [dict create] dict set overrides @id -id "::punk::pipe::about" dict set overrides @cmd -name "punk::pipe::about" dict set overrides @cmd -help [string trim [punk::args::lib::tstr { About punk::pipe }] \n] dict set overrides topic -choices [list {*}[punk::pipe::argdoc::about_topics] *] dict set overrides topic -choicerestricted 1 dict set overrides topic -default [punk::pipe::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] lappend PUNKARGS [list $newdef] proc about {args} { package require punk::args #standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on set argd [punk::args::parse $args withid ::punk::pipe::about] lassign [dict values $argd] _leaders opts values _received punk::args::package::standard_about -package_about_namespace ::punk::pipe::argdoc {*}$opts {*}[dict get $values topic] } } # end of sample 'about' function # == === === === === === === === === === === === === === === 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::pipe } # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Ready package provide punk::pipe [tcl::namespace::eval punk::pipe { variable pkg punk::pipe variable version set version 999999.0a1.0 }] return #*** !doctools #[manpage_end]