diff --git a/scriptlib/showargs.tcl b/scriptlib/showargs.tcl index 25bb97ec..31802c4f 100644 --- a/scriptlib/showargs.tcl +++ b/scriptlib/showargs.tcl @@ -1,7 +1,8 @@ #puts -nonewline stdout "info script\r\n" #puts stdout "[info script]" -puts stdout "::argc" -puts stdout $::argc -puts stdout "::argv" -puts stdout "$::argv" - +puts stdout "argc: $::argc" +puts stdout "argv one arg per line, each line followed by dotted line." +foreach a $::argv { + puts stdout $a + puts stdout [string repeat - 40] +} diff --git a/scriptlib/showargsplus.tcl b/scriptlib/showargsplus.tcl new file mode 100644 index 00000000..9833034b --- /dev/null +++ b/scriptlib/showargsplus.tcl @@ -0,0 +1,28 @@ +puts stdout "argc: $::argc" +puts stdout "argv one arg per line, each line followed by dotted line." +foreach a $::argv { + puts stdout $a + puts stdout [string repeat - 40] +} +flush stdout +puts stdout "****** raw args******" +package require twapi +set rawcmdline [twapi::get_process_commandline [pid]] +puts stdout $rawcmdline +puts stdout "****** ******" +flush stdout +set cl_to_argv [twapi::get_command_line_args $rawcmdline] +puts stdout "======twapi CommandLineToArgvW interpretation of args =====" +foreach ca $cl_to_argv { + puts stdout $ca + puts stdout [string repeat = 40] +} +puts stdout \n +package require punk::winrun +set cl_to_argv [punk::winrun::unquote_wintcl $rawcmdline] +puts stdout "======winrun::unquote_wintcl interpretation of args =====" +foreach ca $cl_to_argv { + puts stdout $ca + puts stdout [string repeat = 40] +} +puts stdout \n diff --git a/src/modules/calc676-999999.0a1.0.tm b/src/modules/calc676-999999.0a1.0.tm new file mode 100644 index 00000000..8466b880 --- /dev/null +++ b/src/modules/calc676-999999.0a1.0.tm @@ -0,0 +1,285 @@ +# -*- 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 calc676 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval calc676 { + # Tcl version of a Calc command for TIP 676. + # + # Prototype for an expression evaluator which does no internal substitution, + # instead expecting any substitutions on its arguments to have been done + # in advance by the usual Tcl mechanisms. To avoid unpleasant surprises + # as warned about by Peter Da Silva, each token must be supplied as a + # separate argument, e.g. calc 2 * abs( $x - $y ) NOT calc 2*abs($x-$y) + # Only numeric and boolean values and operations are supported as there + # is no way to distinguish arbitrary string values from operators they + # might happen to mimic. + + variable tokens + variable tokpos + variable depth + + proc calc args { + variable tokens + variable tokpos + variable depth + + if {[llength $args] == 0} {error "Calc: nothing to calculate"} + set tokens $args + set tokpos 0 + set depth 0 + set code [parse 0] + #puts "GENERATED CODE:\n$code" + set result [::tcl::unsupported::assemble $code] + #puts "OUTPUT = '$result'" + return $result + } + + # Pratt Parser loosely based on https://www.rosettacode.org/wiki/Arithmetic_evaluation#Nim + + variable inprec + variable incode + + # Define infix operators, their precedences and bytecodes + foreach {op prec code} { + ) 0 - + , 0 - + ? 1 - + : 1 - + || 2 lor + && 3 land + | 4 bitor + ^ 5 bitxor + & 6 bitand + == 7 eq + != 7 neq + < 8 lt + > 8 gt + <= 8 le + >= 8 ge + << 9 lshift + >> 9 rshift + + 10 add + - 10 sub + * 11 mult + / 11 div + % 11 mod + ** 12 expon + } { + set inprec($op) $prec + set incode($op) $code + } + + variable precode + # Define prefix operators and their bytecodes + foreach {op code} { + + uplus + - uminus + ! not + ~ bitnot + } { + set precode($op) $code + } + variable preprec + + # Prefix ops all have the same precedence + set preprec 13 + + # Parse expression until we hit an operator with precedence lower than min_prec. + # The expression is supplied as a list of tokens in the global var tokens. + # The current position in the input is in global var tokpos. + # Returns the TAL bytecode to evaluate the expression. + proc parse min_prec { + variable inprec + variable incode + variable tokens + variable tokpos + variable depth + + set token [lindex $tokens $tokpos] + set dep [incr depth] + #puts "[string repeat { } $dep]PARSE min_prec=$min_prec tokpos=$tokpos token='$token'" + incr tokpos + set opcodes [parsePrefix $token] + set depth $dep + + while {$tokpos < [llength $tokens]} { + + set token [lindex $tokens $tokpos] + if {[info exists inprec($token)]} { + set tok_prec $inprec($token) + } else { + error "Calc: expected operator but found '$token'" + } + #puts "[string repeat { } $dep]PARSE token=$token tok_prec=$tok_prec" + if {$tok_prec < $min_prec} { + break + } + # Binary ops are left-associative except for ** + if {$tok_prec == $min_prec && $token ne "**"} { + break + } + # if-then-else needs special handling + incr tokpos + if {$token eq "?"} { + append opcodes [parseTernary] + continue + } + # Infix operator + append opcodes [parse $tok_prec] "$incode($token); " + } + #puts "[string repeat { } $dep]PARSE opcodes='$opcodes'" + set depth [expr {$dep - 1}] + return $opcodes + } + + # Parse expression up to the first operator at the same level of parentheses. + # Returns the bytecode to evaluate the subexpression. + proc parsePrefix token { + variable preprec + variable precode + variable tokens + variable tokpos + variable depth + + set dep [incr depth] + #puts "[string repeat { } $dep]PARSEPREFIX token=`$token` tokpos=$tokpos" + + # Is it a number? In C would use Tcl_GetNumberFromObj() here + if {[string is entier $token] || [string is double $token]} { + return "push $token; " + } + # Is it boolean? In C would use Tcl_GetBoolean() here + if {[string is boolean $token]} { + return "push $token; " + } + # Parenthesised subexpression? + if {$token eq "("} { + set opcodes [parse 0] + set token [lindex $tokens $tokpos] + if {$token eq ")"} { + incr tokpos + return $opcodes + } + error "Calc: expected ')' but found '$token'" + } + # Unary operator? + if {$token in {+ - ! ~}} { + return "[parse $preprec]$precode($token); " + } + # Function call? + if {[regexp {^([[:alpha:]]+)\($} $token - name]} { + set fun [namespace which tcl::mathfunc::$name] + if {$fun ne {}} { + set opcodes "push $fun; " + append opcodes [parseFuncArgs] + return $opcodes + } + } + error "Calc: expected start of expression but found '$token'" + } + + # Parse zero or more arguments to a math function. The arguments are + # expressions separated by commas and terminated by a closing parenthesis. + # Returns the bytecode to evaluate the arguments and call the function. + proc parseFuncArgs {} { + variable tokens + variable depth + variable tokpos + + set dep [incr depth] + #puts "[string repeat { } $dep]PARSEFUNCARGS tokpos=$tokpos" + + set token [lindex $tokens $tokpos] + set arg_num 1 + while 1 { + if {$token eq ")"} { + incr tokpos + append opcodes "invokeStk $arg_num; " + return $opcodes + } + append opcodes [parse 0] + incr arg_num + + set token [lindex $tokens $tokpos] + switch $token { + , { incr tokpos } + ) {} + default { + error "Calc: expected ')' or ',' but found '$token'" + } + } + } + } + + # We have just seen the '?' of an if-then-else, so parse the rest of that. + # Returns the bytecode to check the previous condition, then evaluate the + # appropriate branch. + proc parseTernary {} { + variable inprec + variable tokens + variable tokpos + variable depth + + set dep [incr depth] + #puts "[string repeat { } $dep]PARSETERNARY tokpos=$tokpos" + + set else else[incr ::labelcount] + set end end$::labelcount + append opcodes "jumpFalse $else; [parse $inprec(:)]" + + set token [lindex $tokens $tokpos] + if {$token ne ":"} { + error "Calc: expected ':' but found '$token'" + } + incr tokpos + + append opcodes "jump $end; label $else; [parse $inprec(:)]" + append opcodes "label $end; nop; " + return $opcodes + } + + + + +} + + + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide calc676 [namespace eval calc676 { + variable version + set version 999999.0a1.0 +}] +return \ No newline at end of file diff --git a/src/modules/calc676-buildversion.txt b/src/modules/calc676-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/calc676-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 7a5efc51..134dee8a 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -127,19 +127,6 @@ namespace eval punk { #variable re_headvar1 {([a-zA-Z:@.(),]+?)(?![^(]*\))(,.*)*$} - #load package and move to namespace of same name - proc pkguse {pkg} { - set ver [package require $pkg] - if {[namespace exists ::$pkg]} { - set out [punk::ns::ns/ / ::$pkg] - append out \n $ver - return $out - } else { - set out $ver - } - return $out - } - interp alias "" use "" punk::pkguse #----------------------------------------------------------------------------------- #strlen is important for testing issues with string representationa and shimmering. #This specific implementation with append (as at 2023-09) is designed to ensure the original str representation isn't changed @@ -449,13 +436,14 @@ namespace eval punk { scan $s %${p}s%s } proc _split_patterns {varspecs} { - set cmdname ::punk::pipecmds::split_patterns_$varspecs + set name_mapped [pipecmd_rhsmapping $varspecs] + set cmdname ::punk::pipecmds::split_patterns_$name_mapped if {$cmdname in [info commands $cmdname]} { return [$cmdname] } set varlist [list] - set var_terminals [list "@" "/" "#" ">"] ;# (> required for insertionspecs at rhs of = & .= ) + set var_terminals [list "@" "/" "#" ">" "<"] ;# (> required for insertionspecs at rhs of = & .= ) #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 @@ -2858,6 +2846,12 @@ namespace eval punk { 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_rhsmapping {rhs} { + return [string map [list " " "" \t ""] $rhs] + } + #same as used in unknown func for initial launch #variable re_assign {^([^\r\n=\{]*)=(.*)} #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} @@ -2872,12 +2866,16 @@ namespace eval punk { #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args" set fulltail $args set homens ::punk::pipecmds - - set pipecmd ${homens}::$scopepattern=$equalsrhs + set rhsmapping [pipecmd_rhsmapping $equalsrhs] + set pipecmd ${homens}::$scopepattern=$rhsmapping #pipecmd could have glob chars - test $pipcmd in the list - not just that info commands returns results. if {$pipecmd in [info commands $pipecmd]} { #puts "==nscaller: '[uplevel 1 [list namespace current]]'" - uplevel 1 [list ::namespace import $pipecmd] + #uplevel 1 [list ::namespace import $pipecmd] + set existing_path [uplevel 1 [list ::namespace path]] + if {$homens ni $existing_path} { + uplevel 1 [list ::namespace path [concat $existing_path $homens]] + } tailcall $pipecmd {*}$args } @@ -2990,7 +2988,7 @@ namespace eval punk { set datasource $v } append script [string map [list $datasource] { - set insertion_data + set insertion_data "" ;#atom could have whitespace }] set needs_insertion 1 @@ -3038,7 +3036,10 @@ namespace eval punk { debug.punk.pipe.compile {match_assign creating proc $pipecmd} 2 uplevel 1 [list ::proc $pipecmd args $script] - uplevel 1 [list ::namespace import $pipecmd] + set existing_path [uplevel 1 [list ::namespace path]] + if {$homens ni $existing_path} { + uplevel 1 [list ::namespace path [concat $existing_path $homens]] + } tailcall $pipecmd {*}$args } @@ -3344,19 +3345,54 @@ namespace eval punk { } } + #exclude quoted whitespace proc arg_is_script_shaped {arg} { - if {[string first " " $arg] >= 0} { - return 1 - } elseif {[string first \n $arg] >= 0} { + if {[string first \n $arg] >= 0} { return 1 } elseif {[string first ";" $arg] >= 0} { return 1 - } elseif {[string first \t $arg] >= 0} { - return 1 + } elseif {[string first " " $arg] >= 0 || [string first \t $arg] >= 0} { + lassign [_rhs_tail_split $arg] _ part2 ;#will have part2 if unquoted whitespace found + if {$part2 eq ""} { + return 0 + } else { + return 1 + } } 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 { + if {$ch eq {'}} { + set inq 1 + } elseif {$ch eq {"}} { + set indq 1 + } elseif {$ch in [list " " \t]} { + #whitespace outside of quoting + break + } + append equalsrhs $ch + } + incr i + } + set tail [string range $fullrhs $i end] + return [list $equalsrhs $tail] + } proc pipeline {segment_op initial_returnvarspec equalsrhs args} { set fulltail $args @@ -3406,33 +3442,28 @@ namespace eval punk { #handle for example: #var1.= var2= "etc" |> string toupper # - #var1 will contain ETC, var2 will contain etc + #var1 will contain ETC (from entire pipeline), var2 will contain etc (from associated segment) # - if {([string first = $next1] >= 0) && (![arg_is_script_shaped $next1]) } { + + if {([set nexteposn [string first = $next1]] >= 0) && (![arg_is_script_shaped $next1]) } { #*SUB* pipeline recursion. #puts "======> recurse based on next1:$next1 " - #set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} - if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $next1 _ nextreturnvarspec nextrhs]} { + if {[string index $next1 $nexteposn-1] eq {.}} { + #var1.= var2.= ... #non pipelined call to self - return result - #debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 4 - #set results [uplevel 1 [list ::punk::pipeline .= $nextreturnvarspec $nextrhs {*}$nexttail]] set results [uplevel 1 [list $next1 {*}$nexttail]] - #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 asssign based on next1:$next1 " - if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { - #non pipelined call to plain = assignment - return result - #debug.punk.pipe {nextreturnvarspec: $nextreturnvarspec nextrhs:$nextrhs tail:$nexttail} 4 - #set results [uplevel 1 [list ::punk::pipeline = $nextreturnvarspec $nextrhs {*}$nexttail]] - set results [uplevel 1 [list $next1 {*}$nexttail]] - #debug.punk.pipe {>>> results: $results} 1 - set d [_multi_bind_result $initial_returnvarspec $results] - return [_handle_bind_result $d] - } + } + #puts "======> recurse assign based on next1:$next1 " + #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { + #} + #non pipelined call to plain = assignment - return result + set results [uplevel 1 [list $next1 {*}$nexttail]] + #debug.punk.pipe {>>> results: $results} 1 + set d [_multi_bind_result $initial_returnvarspec $results] + return [_handle_bind_result $d] } } @@ -3600,8 +3631,10 @@ namespace eval punk { } set insertion_patterns [_split_equalsrhs $rhs] ;#raises error if rhs of positionspec not like /* or @* - #puts stdout ">>> insertion_patterns $insertion_patterns" set segment_has_insertions [expr {[llength $insertion_patterns] > 0}] + #if {$segment_has_insertions} { + # puts stdout ">>> $segment_members insertion_patterns $insertion_patterns" + #} debug.punk.pipe.var {segment_has_insertions: $insertion_patterns} 5 debug.punk.pipe.rep {[rep_listname segment_members]} 4 @@ -3650,8 +3683,8 @@ namespace eval punk { set segment_members_filled [list] set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign - - set cmdname "::punk::pipecmds::insertion_$rhs" + set rhsmapped [pipecmd_rhsmapping $rhs] + set cmdname "::punk::pipecmds::insertion_$rhsmapped" #commandname can contain glob chars - must search for exact membership in 'info commands' result. if {$cmdname ni [info commands $cmdname]} { @@ -3671,7 +3704,7 @@ namespace eval punk { if {[string length $indexspec]} { error "pipesyntax - index not supported on atom" "pipeline $segment_op $initial_returnvarspec $equalsrhs $args" [list pipesyntax index_on_literal] } - append insertion_script \n "set insertion_data $v" ;#sub in shortened $v now -i.e use atom value itself (string within single quotes) + append insertion_script \n "set insertion_data [list $v]" ;#sub in shortened $v now -i.e use atom value itself (string within single quotes) } elseif {[string is double -strict $v]} { #don't treat numbers as variables if {[string length $indexspec]} { @@ -3720,17 +3753,17 @@ namespace eval punk { append insertion_script \n {set segmenttail} append insertion_script \n "}" #puts stderr "$insertion_script" - debug.punk.pipe.compile {creating proc ::punk::pipecmds::insertion_$rhs } 4 + debug.punk.pipe.compile {creating proc ::punk::pipecmds::insertion_$rhsmapped } 4 eval $insertion_script } - set segment_members_filled [::punk::pipecmds::insertion_$rhs $dict_tagval [lindex [list $segmenttail [unset segmenttail]] 0] ] + set segment_members_filled [::punk::pipecmds::insertion_$rhsmapped $dict_tagval [lindex [list $segmenttail [unset segmenttail]] 0] ] #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] + set rhs [string map $dict_tagval $rhs] ;#obsolete? debug.punk.pipe.rep {segment_members_filled rep: [rep $segment_members_filled]} 4 @@ -4170,7 +4203,9 @@ namespace eval punk { } #regexp $punk::re_assign $hd _ pattern equalsrhs #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=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail + regexp {^([^\t\r\n=]*)\=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_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 @@ -4191,15 +4226,17 @@ namespace eval punk { error "unknown_assign_dispatch: namespace '$ns' not found. (Note that pipeline lhs variables cannot be namespaced)" } else { set nscaller [uplevel 1 [list ::namespace current]] - set commands [uplevel 1 [list ::info commands $pattern=$equalsrhs]] ;#uplevel - or else we are checking from perspective of this namespace ::punk + #jmn + set rhsmapped [pipecmd_rhsmapping $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=$equalsrhs" in $commands} { + 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...? #tailcall $pattern=$equalsrhs {*}$args - tailcall $pattern=$equalsrhs {*}$tail + tailcall $pattern=$rhsmapped {*}$tail } } #puts "--->nscurrent [uplevel 1 [list ::namespace current]]" @@ -4216,19 +4253,11 @@ namespace eval punk { #e.g x=a\nb c #x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained # - know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} - - know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #know {[regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + #know {[regexp {^{([^\t\r\n=]*)\=([^ \t\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^([^\t\r\n=]*)\=([^\r\n]*)} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} + know {[regexp {^{([^\t\r\n=]*)\=([^\r\n]*)}} [lindex $args 0] matchedon pattern equalsrhs]} {tailcall ::punk::_unknown_assign_dispatch $matchedon $pattern $equalsrhs {*}$args} - #variable re_assign {^([^\r\n=\{]*)=(.*)} - #know {[regexp $punk::re_assign [lindex $args 0 0] partzerozero varspecs rhs]} { - # set tail [lassign $args hd] - # if {$hd ne $partzerozero} { - # regexp $punk::re_assign $hd _ varspecs rhs - # } - # # tailcall so match_assign runs at same level as the unknown proc - # tailcall ::punk::match_assign $varspecs $rhs $tail - #} proc ::punk::_unknown_compare {val1 val2 args} { @@ -4272,6 +4301,9 @@ namespace eval punk { # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] # } # + + + proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} { set argstail [lassign $args hd] @@ -4286,17 +4318,20 @@ namespace eval punk { #regexp $punk::re_assign $hd _ pattern equalsrhs #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=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail + + regexp {^([^ \t\r\n=\{]*)\.=([^\r\n]*)} $hd _ pattern fullrhs + lassign [_rhs_tail_split $fullrhs] equalsrhs argstail } #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail - return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] } #variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} - know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + #know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} + know {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} [lindex $args 0] partzerozero varspecs rhs]} {tailcall punk::_unknown_dot_assign_dispatch $partzerozero $varspecs $rhs {*}$args} #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} { # set argstail [lassign $args hd] @@ -4323,18 +4358,7 @@ namespace eval punk { tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] } - #maint - punk::arg_is_script_shaped (inlined) - if {[string first " " $assign] >= 0} { - set is_script 1 - } elseif {[string first \n $assign] >= 0} { - set is_script 1 - } elseif {[string first ";" $assign] >= 0} { - set is_script 1 - } elseif {[string first \t $assign] >= 0} { - set is_script 1 - } else { - set is_script 0 - } + set is_script [punk::arg_is_script_shaped $assign] if {!$is_script && [string index $assign end] eq "="} { #set re_dotequals {^([^ \t\r\n=\{]*)\.=$} @@ -5069,6 +5093,7 @@ namespace eval punk { #todo - in thread #todo - streaming version proc dirfiles_dict {{searchspec ""}} { + package require vfs #we don't want to normalize.. #for example if the user supplies ../ we want to see ../result if {[file pathtype $searchspec] eq "relative"} { @@ -5340,6 +5365,18 @@ namespace eval punk { interp alias {} ./new {} punk::d/new interp alias {} d/new {} punk::d/new + #todo use unknown to allow d/~c:/etc ?? + proc d/~ {args} { + set home $::env(HOME) + set target [file join $home {*}$args] + if {![file isdirectory $target]} { + error "Folder $target not found" + } + d/ $target + } + interp alias {} ./~ {} punk::d/~ + interp alias {} d/~ {} punk::d/~ + #pass in base and platform to head towards purity/testability. #this function can probably never be pure in such a simple form - as it needs to read state from the os storage system configuration @@ -6368,11 +6405,14 @@ namespace eval punk { proc help_chunks {args} { set chunks [list] set linesep [string repeat - 76] + set mascotblock " " catch { package require patternpunk - #puts -nonewline stderr [>punk . rhs] - lappend chunks [list stderr [>punk . rhs]] + #lappend chunks [list stderr [>punk . rhs]] + append mascotblock [>punk . banner -title "Punk Shell" -left Tcl -right [package provide Tcl]] } + + set text "" set known $::punk::config::known_punk_env_vars append text $linesep\n @@ -6393,7 +6433,7 @@ namespace eval punk { lappend chunks [list stdout $text] set text "" - append text "Punk commands:\n" + append text "Punk core navigation commands:\n" append text " help\n" #todo - load from source code annotation? @@ -6402,10 +6442,10 @@ namespace eval punk { lappend cmdinfo [list ./ "view/change directory"] lappend cmdinfo [list ../ "go up one directory"] lappend cmdinfo [list ./new "make new directory and switch to it"] - lappend cmdinfo [list :/ "view/change namespace (accepts ns path globs e.g **::*get* to match comands at any level )"] - lappend cmdinfo [list :// "view/change namespace (with command listing)"] - lappend cmdinfo [list ::/ "go up one namespace"] - lappend cmdinfo [list :/new "make child namespace and switch to it"] + lappend cmdinfo [list n/ "view/change namespace (accepts ns path globs e.g **::*get* to match comands at any level )"] + lappend cmdinfo [list n// "view/change namespace (with command listing)"] + lappend cmdinfo [list nn/ "go up one namespace"] + lappend cmdinfo [list n/new "make child namespace and switch to it"] set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *] set descr [lsearch -all -inline -index 1 -subindices $cmdinfo *] @@ -6417,8 +6457,25 @@ namespace eval punk { append text " [overtype::left $col1 $c][overtype::left $col2 $d]" \n } + set indent " " + set sep " " + if {[catch { + package require textblock + set introblock [textblock::join\ + [textblock::join\ + [textblock::join\ + $indent\ + $mascotblock\ + ]\ + $sep\ + ]\ + $text\ + ] + }] } { + set introblock $text + } - lappend chunks [list stdout $text] + lappend chunks [list stdout $introblock] if {[punkrepl::has_script_var_bug]} { append text "Has script var bug! (string rep for list variable in script generated when script changed)" @@ -6671,6 +6728,8 @@ namespace eval punk { 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 {} ./ {} punk::d/ interp alias {} ../ {} punk::dd/ interp alias {} d/ {} punk::d/ diff --git a/src/modules/punk/cap-999999.0a1.0.tm b/src/modules/punk/cap-999999.0a1.0.tm index 8987a73e..bdcbbc31 100644 --- a/src/modules/punk/cap-999999.0a1.0.tm +++ b/src/modules/punk/cap-999999.0a1.0.tm @@ -181,10 +181,10 @@ namespace eval punk::cap { if {[file isdirectory $tpath]} { dict set folderdict $tpath [list source $pkg sourcetype package] } else { - puts stderr "punk::mix template_folders WARNING - unable to determine base folder for package '$pkg' which is registered with punk::mix as a provider of 'templates' capability" + puts stderr "punk::cap::templates::folders WARNING - unable to determine base folder for package '$pkg' which is registered with punk::mix as a provider of 'templates' capability" } } else { - puts stderr "punk::mix template_folders WARNING - registered pkg 'pkg' has capability 'templates' but no 'relpath' key - unable to use as source of templates" + puts stderr "punk::cap::templates::folders WARNING - registered pkg 'pkg' has capability 'templates' but no 'relpath' key - unable to use as source of templates" } } } diff --git a/src/modules/punk/char-999999.0a1.0.tm b/src/modules/punk/char-999999.0a1.0.tm index 58115b52..61b68b55 100644 --- a/src/modules/punk/char-999999.0a1.0.tm +++ b/src/modules/punk/char-999999.0a1.0.tm @@ -1675,6 +1675,7 @@ namespace eval punk::char { } set search_this_and_that $args set charcount 0 + set width_results [dict create] puts stdout "calibrating using terminal cursor movements.." foreach charsetname $matched_names { if {[llength $search_this_and_that]} { @@ -1692,7 +1693,6 @@ namespace eval punk::char { if {![dict size $charset_dict]} { continue } - set width_results [dict create] dict for {hex inf} $charset_dict { set ch [format %c 0x$hex] set twidth "" diff --git a/src/modules/punk/console-999999.0a1.0.tm b/src/modules/punk/console-999999.0a1.0.tm index 62f8fa70..7249d51f 100644 --- a/src/modules/punk/console-999999.0a1.0.tm +++ b/src/modules/punk/console-999999.0a1.0.tm @@ -81,6 +81,36 @@ namespace eval punk::console { } } + proc enable_mouse {} { + puts -nonewline stdout \x1b\[?1000h + puts -nonewline stdout \x1b\[?1003h + puts -nonewline stdout \x1b\[?1015h + puts -nonewline stdout \x1b\[?1006h + flush stdout + } + proc disable_mouse {} { + puts -nonewline stdout \x1b\[?1000l + puts -nonewline stdout \x1b\[?1003l + puts -nonewline stdout \x1b\[?1015l + puts -nonewline stdout \x1b\[?1006l + flush stdout + } + proc enable_bracketed_paste {} { + puts -nonewline stdout \x1b\[?2004h + } + proc disable_bracketed_paste {} { + puts -nonewline stdout \x1b\[?2004l + } + proc start_application_mode {} { + #need loop to read events? + puts -nonewline stdout \x1b\[?1049h ;#alt screen + enable_mouse + #puts -nonewline stdout \x1b\[?25l ;#hide cursor + puts -nonewline stdout \x1b\[?1003h\n + enable_bracketed_paste + + } + namespace eval internal { proc abort_if_loop {{failmsg ""}} { #puts "il1 [info level 1]" diff --git a/src/modules/punk/mix/commandset/layout-999999.0a1.0.tm b/src/modules/punk/mix/commandset/layout-999999.0a1.0.tm index 25d95d73..5d724260 100644 --- a/src/modules/punk/mix/commandset/layout-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/layout-999999.0a1.0.tm @@ -41,9 +41,9 @@ namespace eval punk::mix::commandset::layout { set template_folder_dict [punk::mix::base::lib::get_template_folders] set tpldirs [list] - dict for {dir folderinfo} $template_folder_dict { - if {[file exists $dir/layouts/$layout]} { - lappend tpldirs $dir + dict for {tdir folderinfo} $template_folder_dict { + if {[file exists $tdir/layouts/$layout]} { + lappend tpldirs $tdir } } if {![llength $tpldirs]} { @@ -75,8 +75,8 @@ namespace eval punk::mix::commandset::layout { set layouts [list] #set tplfolderdict [punk::cap::templates::folders] set tplfolderdict [punk::mix::base::lib::get_template_folders] - dict for {tpldir folderinfo} $tplfolderdict { - set layout_base $tpldir/layouts + dict for {tdir folderinfo} $tplfolderdict { + set layout_base $tdir/layouts #collect all layouts and use lsearch glob rather than the filesystem glob (avoid issues with dotted folder names) set all_layouts [lsort [glob -nocomplain -dir $layout_base -type d -tail *]] foreach match [lsearch -all -inline $all_layouts $glob] { @@ -107,7 +107,7 @@ namespace eval punk::mix::commandset::layout { set layoutfolder [lindex $layouts_found end] if {![file isdirectory $layoutfolder]} { - puts stderr "layout '$layout' not found in $tpldir/layouts" + puts stderr "layout '$layout' not found in /layouts within one of template_folders. (get_template_folder returned: $tplfolderdict)" } set file_list [list] util::foreach-file $layoutfolder path { diff --git a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm index f7be001b..86181aac 100644 --- a/src/modules/punk/mix/commandset/module-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/module-999999.0a1.0.tm @@ -349,9 +349,9 @@ namespace eval punk::mix::commandset::module { set opt_scriptpath [dict get $opts -scriptpath] set module_tfolders [list] - set tfolders [punk::mix::base::lib::get_template_folders $opt_scriptpath] - foreach tf $tfolders { - lappend module_tfolders [file join $tf module] + set tfolderdict [punk::mix::base::lib::get_template_folders $opt_scriptpath] + dict for {tdir folderinfo} $tfolderdict { + lappend module_tfolders [file join $tdir module] } 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 cabd62ca..335f6bb2 100644 --- a/src/modules/punk/mix/commandset/project-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/project-999999.0a1.0.tm @@ -160,16 +160,16 @@ namespace eval punk::mix::commandset::project { set template_folder_dict [punk::mix::base::lib::get_template_folders] set tpldirs [list] - dict for {dir folderinfo} $template_folder_dict { - if {[file exists $dir/layouts/$opt_layout]} { - lappend tpldirs $dir + dict for {tdir folderinfo} $template_folder_dict { + if {[file exists $tdir/layouts/$opt_layout]} { + lappend tpldirs $tdir } } if {![llength $tpldirs]} { puts stderr "layout '$opt_layout' was not found in template dirs" puts stderr "searched [dict size $template_folder_dict] template folders" - dict for {dir folderinfo} $template_folder_dict { - puts stderr " - $dir $folderinfo" + dict for {tdir folderinfo} $template_folder_dict { + puts stderr " - $tdir $folderinfo" } return } diff --git a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm index 0a536bc8..c26cbf10 100644 --- a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm @@ -245,11 +245,13 @@ namespace eval punk::mix::commandset::scriptwrap { set templatename $opt_template } - set template_folder_dict [punk::mix::template_folders] + + + set template_folder_dict [punk::mix::base::lib::get_template_folders] set tpldirs [list] - dict for {dir pkg} $template_folder_dict { - if {[file exists $dir/utility/scriptappwrappers/$templatename]} { - lappend tpldirs $dir + dict for {tdir tsourceinfo} $template_folder_dict { + if {[file exists $tdir/utility/scriptappwrappers/$templatename]} { + lappend tpldirs $tdir } } @@ -261,12 +263,20 @@ namespace eval punk::mix::commandset::scriptwrap { append msg \n "Searched [dict size $template_folder_dict] template dirs" error $msg } - set libwrapper_folder_default [file join [lindex $tpldir end] utility scriptappwrappers] - set wrapper_template [file join $libwrapper_folder_default $templatename] + + #last pkg with templates cap which was loaded has highest precedence + set wrapper_template "" + foreach tdir [lreverse $tpldirs] { + set ftest [file join $tdir utility scriptappwrappers $templatename] + if {[file exists $ftest]} { + set wrapper_template $ftest + break + } + } } - if {![file exists $wrapper_template]} { - error "wrap_in_multishell: unable to find multishell template at $wrapper_template" + if {$wrapper_template eq "" || ![file exists $wrapper_template]} { + error "wrap_in_multishell: unable to find multishell template $templatename in template folders [concat $tpldirs $customwrapper_folder]" } @@ -434,11 +444,11 @@ namespace eval punk::mix::commandset::scriptwrap { } } - set template_folder_dict [punk::mix::template_folders] + set template_folder_dict [punk::mix::base::lib::get_template_folders] set tpldirs [list] - dict for {dir pkg} $template_folder_dict { - if {[file exists $dir/utility/scriptappwrappers]} { - lappend tpldirs $dir + dict for {tdir tsourceinfo} $template_folder_dict { + if {[file exists $tdir/utility/scriptappwrappers]} { + lappend tpldirs $tdir } } foreach tpldir $tpldirs { diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index c9683723..88d3c76e 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -26,75 +26,78 @@ namespace eval ::punk_dynamic::ns { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ namespace eval punk::ns { variable ns_current "::" + variable ns_re_cache [dict create] ;#cache regular expressions used in globmatchns namespace export nsjoin nsprefix nstail nsparts nseval nsimport_noclobber corp #leading colon makes it hard (impossible?) to call directly if not within the namespace - #todo - change semantics of args - it's not particularly useful to pass namespaces as separated items - would be better to accept options (e.g nslist option -types) - proc ns/ {v args} { + proc ns/ {v {ns_or_glob ""} args} { variable ns_current ;#change active ns of repl by setting ns_current set ns_caller [uplevel 1 {::namespace current}] #puts stderr "ns_cur:$ns_current ns_call:$ns_caller" - if {![llength $args]} { - #set out [get_nslist $ns_current] - if {$v eq "/"} { - set out [nslist [nsjoin $ns_current *] -types [list children]] + + set types [list all] + set nspathcommands 0 + if {$v eq "/"} { + set types [list children] + } + if {$v eq "///"} { + set nspathcommands 1 + } + + #todo - cooperate with repl? + set out "" + if {$ns_or_glob eq ""} { + set is_absolute 1 + set ns_queried $ns_current + set out [nslist [nsjoin $ns_current *] -types $types -nspathcommands $nspathcommands] + } else { + set is_absolute [string match ::* $ns_or_glob] + set has_globchars [regexp {[*?]} $ns_or_glob] + if {$is_absolute} { + if {!$has_globchars} { + if {![namespace exists $ns_or_glob]} { + error "cannot change to namespace $ns_or_glob" + } + set ns_current $ns_or_glob + set ns_queried $ns_current + tailcall ns/ $v "" + } else { + set ns_queried $ns_or_glob + set out [nslist $ns_or_glob -types $types -nspathcommands $nspathcommands] + } } else { - set out [nslist [nsjoin $ns_current *] -types [list all]] + if {!$has_globchars} { + set nsnext [nsjoin $ns_current $ns_or_glob] + if {![namespace exists $nsnext]} { + error "cannot change to namespace $ns_or_glob" + } + set ns_current $nsnext + set ns_queried $nsnext + set out [nslist [nsjoin $nsnext *] -types $types -nspathcommands $nspathcommands] + } else { + set ns_queried [nsjoin $ns_current $ns_or_glob] + set out [nslist [nsjoin $ns_current $ns_or_glob] -types $types -nspathcommands $nspathcommands] + } } - #todo - cooperate with repl - - set ns_display "\n$ns_current" + } + set ns_display "\n$ns_queried" + if {$ns_current eq $ns_queried} { if {$ns_current in [info commands $ns_current] } { if {![catch [list namespace ensemble configure $ns_current] ensemble_info]} { if {[llength $ensemble_info] > 0} { #this namespace happens to match ensemble command. #todo - keep cache of encountered ensembles from commands.. and examine namespace in the configure info. - set ns_display "\n[a+ yellow bold]$ns_current[a+]" + set ns_display "\n[a+ yellow bold]$ns_current (ensemble)[a+]" } } } - append out $ns_display - return $out - } else { - set atail [lassign $args a1] - if {$a1 in [list :: ""]} { - set ns_current :: - tailcall ns/ $v {*}$atail - } - set is_absolute [string match ::* $a1] - if {$is_absolute} { - if {![llength $atail] && [regexp {[*?]} $a1]} { - #set out [get_nslist -match $a1] - set out [nslist $a1] - append out "\n$a1" - return $out - } - set nsparent [nsprefix $a1] - set nstail [nstail $a1] - if {[nseval $nsparent [list ::namespace exists $nstail]]} { - set ns_current $a1 - tailcall ns/ $v {*}$atail - } - error "cannot change to namespace $a1" - } else { - set nsnext [nsjoin $ns_current $a1] - if {![llength $atail] && [regexp {[*?]} $a1]} { - #set out [get_nslist -match $nsnext] - set out [nslist $nsnext] - append out "\n$nsnext" - return $out - } - - if {[nseval $ns_current [list ::namespace exists $a1]]} { - set ns_current $nsnext - tailcall ns/ $v {*}$atail - } else { - error "cannot change to namespace $nsnext" - } - } } + append out $ns_display + return $out + + } @@ -381,7 +384,17 @@ namespace eval punk::ns { return "^[join $pats ::]\$" } proc globmatchns {glob path} { - return [regexp [nsglob_as_re $glob] $path] + #the total set of namespaces is *generally* reasonably bounded so we could just cache all globs, perhaps with some pretty high limit for sanity.. (a few thousand?) review - memory cost? + # Tcl (reportedly https://wiki.tcl-lang.org/page/regexp) only caches 'up to 30'dynamically - but should cache more if more stored. + variable ns_re_cache + if {![dict exists $ns_re_cache $glob]} { + if {[dict size $ns_re_cache] > 4200} { + #shimmer dict to list and back doesn't seem to affect internal rep of regexp items therein. + set ns_re_cache [lrange $ns_re_cache 400 end] ;#chop 200 items off beginning of dict + } + dict set ns_re_cache $glob [nsglob_as_re $glob] + } + return [regexp [dict get $ns_re_cache $glob] $path] } proc nstree {{location ""}} { @@ -392,6 +405,9 @@ namespace eval punk::ns { list_as_lines [nstree_list $location] } + + #important: add tests for tricky cases - e.g punk::m**::util vs punk::m*::util vs punk::m*::**::util - these should all be able to return different results depending on namespace structure. + #e.g punk::m**::util will return punk::mix::util but punk::m*::**::util will not because punk::mix::util is too short to match. Both will return deeper matches such as: punk::mix::commandset::repo::util proc nstree_list {location args} { package require struct::list #puts "> nstree_list $location $args" @@ -408,28 +424,38 @@ namespace eval punk::ns { # -- ---- --- --- --- --- set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $location]] + set has_globchars [regexp {[*?]} $ns_absolute] ;#don't use regexes on plain namespaces with no glob chars + if {!$has_globchars && !$allbelow && ![llength $subnslist]} { + #short circuit trivial case + return [list $location] + } + set base "" - set tailparts "" + set tailparts [list] if {$CALLDEPTH == 0} { set parts [nsparts $ns_absolute] lset parts 0 :: set idx 0 - foreach seg $parts { - if {![regexp {[*?]} $seg]} { - set base [nsjoin $base $seg] - } else { - set tailparts [lrange $parts $idx end] - break + if {$has_globchars} { + foreach seg $parts { + if {![regexp {[*?]} $seg]} { + set base [nsjoin $base $seg] + } else { + set tailparts [lrange $parts $idx end] + break + } + incr idx } - incr idx + } else { + set base $ns_absolute } } else { set base $location set tailparts $subnslist } if {![namespace exists $base]} { - return "" + return [list] } #set parent [nsprefix $ns_absolute] #set tail [nstail $ns_absolute] @@ -447,7 +473,6 @@ namespace eval punk::ns { set nslist [nstree_list $base -subnslist {} -allbelow 1] } elseif {[regexp {[*]{2}$} $nextglob]} { set nslist [list] - #JMN lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] foreach ch $nsmatches { lappend nslist $ch @@ -455,11 +480,17 @@ namespace eval punk::ns { lappend nslist {*}[nstree_list $ch -subnslist [list "**" {*}[lrange $tailparts 1 end]] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow 0] } } else { + #lsearch with -glob ok even if nextglob has no globchars (no discernable speed diff, and earlier parts may have globchars anyway) set nslist [list] lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] - foreach ch $nsmatches { - lappend nslist $ch - lappend nslist {*}[nstree_list $ch -subnslist [lrange $tailparts 1 end] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow $allbelow] + if {[llength $tailparts] >1 || $allbelow} { + foreach ch $nsmatches { + lappend nslist $ch + lappend nslist {*}[nstree_list $ch -subnslist [lrange $tailparts 1 end] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow $allbelow] + } + } else { + #if only one tailpart remaining and not $allbelow - then we already have what we need + set nslist $nsmatches } } } else { @@ -481,25 +512,25 @@ namespace eval punk::ns { if 0 { - set nextglob [lindex $tailparts 0] - if {$nextglob ne "**"} { - set nslist [list] - if {[llength $tailparts]} { - set nsmatches [list] - #lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]::*] - lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] - } else { - set nsmatches $allchildren - } - #return + set nextglob [lindex $tailparts 0] + if {$nextglob ne "**"} { + set nslist [list] + if {[llength $tailparts]} { + set nsmatches [list] + #lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]::*] + lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] + } else { + set nsmatches $allchildren + } + #return - foreach ch $nsmatches { - lappend nslist $ch - lappend nslist {*}[nstree_list $ch -subnslist [lrange $tailparts 1 end] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow $allbelow] + foreach ch $nsmatches { + lappend nslist $ch + lappend nslist {*}[nstree_list $ch -subnslist [lrange $tailparts 1 end] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow $allbelow] + } + } else { + set nslist [nstree_list $base -subnslist {} -allbelow 1] } - } else { - set nslist [nstree_list $base -subnslist {} -allbelow 1] - } } #foreach ns $nslist { @@ -514,15 +545,27 @@ namespace eval punk::ns { #puts stderr "> adding $base to $nslist" set nslist [list $base {*}$nslist] } - - if {$allbelow} { - foreach ns $nslist { - if {[globmatchns ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} { - lappend nslist_filtered $ns - } - } + if {$has_globchars} { + if {$allbelow} { + foreach ns $nslist { + if {[globmatchns ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} { + lappend nslist_filtered $ns + } + } + } else { + set nslist_filtered [struct::list::Lfilter $nslist [list globmatchns ${ns_absolute}]] + } } else { - set nslist_filtered [struct::list::Lfilter $nslist [list globmatchns ${ns_absolute}]] + if {$allbelow} { + foreach ns $nslist { + if {[string equal ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} { + lappend nslist_filtered $ns + } + } + } else { + #set nslist_filtered [struct::list::Lfilter $nslist [list string match ${ns_absolute}]] + set nslist_filtered [list $ns_absolute] + } } return $nslist_filtered } @@ -805,20 +848,31 @@ namespace eval punk::ns { proc nslist {{glob "*"} args} { set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $glob]] if {[dict exists $args -match]} { + #review - presumably this is due to get_nslist taking -match? error "nslist requires positional argument 'glob' instead of -match option" } set defaults [dict create\ - -match $ns_absolute + -match $ns_absolute\ + -nspathcommands 0\ ] + package require textblock set opts [dict merge $defaults $args] - set ns_matches [get_ns_dicts $ns_absolute] + # -- --- --- + set opt_nspathcommands [dict get $opts -nspathcommands] + # -- --- --- + + + set ns_matches [get_ns_dicts $ns_absolute -nspathcommands $opt_nspathcommands] set with_results [list] foreach nsdict $ns_matches { if {[dict get $nsdict itemcount]>0} { lappend with_results $nsdict } } + + #special case when glob is for * - we should also consider existence of item(s) namespacepath as a 'result' + set count_with_results [llength $with_results] set output "" foreach nsdict $with_results { @@ -832,10 +886,28 @@ namespace eval punk::ns { } #if multiple results or if there is only 1 result - but we are path-globbing - then we need to show location if {$count_with_results > 1 || [regexp {[*?]} [nsprefix $ns_absolute]]} { - append output \n [dict get $nsdict location] \n + append output \n [dict get $nsdict location] } - append output $block - if {$count_with_results > 1 } { + if {[string length $block]} { + append output \n $block + } + 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]]" + } else { + append path_text \n " also resolving cmds in namespace paths:" + set nspathdict [dict get $nsdict namespacepath] + dict for {k v} $nspathdict { + set cmds [dict get $v commands] + append path_text \n " path: $k" + append path_text \n " cmds: $cmds" + } + } + append output $path_text + set path_text_width [textblock::width $path_text] + append output \n [string repeat - [expr {max($width,$path_text_width)}]] + } elseif {$count_with_results > 1 && $width > 0 } { append output \n [string repeat - $width] } } @@ -853,28 +925,26 @@ namespace eval punk::ns { #glob chars in the path will result in multiple namespaces being matched #e.g ::tcl::*::d* will match commands beginning with d and child namespaces beginning with d in any namespaces 1 below ::tcl proc get_ns_dicts {fq_glob args} { + #JMN #puts stderr "get_ns_dicts $fq_glob" set glob_is_absolute [expr {[string match ::* $fq_glob]}] if {!$glob_is_absolute} { error "get_ns_dicts requires fully-qualified namespace glob e.g ::*" } + set has_globchars [regexp {[*?]} $fq_glob] + set defaults [dict create\ -allbelow 0\ + -nspathcommands 1\ ] set opts [dict merge $defaults $args] # -- --- --- --- --- --- --- --- --- --- --- --- set allbelow [dict get $opts -allbelow] + set nspathcommands [dict get $opts -nspathcommands] # -- --- --- --- --- --- --- --- --- --- --- --- - set location [nsprefix $fq_glob] - set glob [nstail $fq_glob] + #set location [nsprefix $fq_glob] set commands [list] - - - # set location [nsprefix $fq_glob] - # set glob [nstail $fq_glob] - # set allchildren [nschildren $location] ; #only returns 1 level deeper - # set commands [.= nscommands [nsjoin ${location} $glob] |> linelist ] set nsglob [nsprefix $fq_glob] set glob [nstail $fq_glob] @@ -894,11 +964,17 @@ namespace eval punk::ns { set nsdict_list [list] foreach ch $report_namespaces { #puts "get_ns_dicts>>> $ch glob:'$glob'" - set allchildren [nschildren $ch] ; #only returns 1 level deeper + if {$allbelow == 0 && !$has_globchars} { + set allchildren [list] + } else { + set allchildren [nschildren $ch] ; #sorted, only returns 1 level deeper + } #nscommands returns exactly one line per entry + a trailing newline. If there is an empty line other than at the end - that is because there is a command named as the empty string. # By default 'linelist' trims 1st and last empty line. Turn off all block trimming with -block {} - set commands [.= nscommands -raw [nsjoin $ch $glob] |> .=> linelist -block {}] + #set commands [.= nscommands -raw [nsjoin $ch $glob] |> linelist -block {}] + set commands [linelist -block {} [nscommands -raw [nsjoin $ch $glob]]] + #by convention - returning just \n represents a single result of the empty string whereas no results #after passing through linelist this becomes {} {} which appears as a list of two empty strings. #this is because there isn't a way to represent unambiguously all 3 cases of: empty-list, list of single empty string, list of two empty strings just using \n separated lines @@ -912,9 +988,22 @@ namespace eval punk::ns { } - +#JMN set location $ch set exportpatterns [namespace eval $location {::namespace export}] + set nspathlist [namespace eval $location {::namespace path}] + set nspathdict [dict create] + if {$nspathcommands} { + foreach pathns $nspathlist { + set pathcommands [lmap v [info commands ${pathns}::*] {namespace tail $v}] + set matched [lsearch -all -inline $pathcommands $glob] + dict set nspathdict $pathns [dict create commands $matched] + } + } else { + foreach pathns $nspathlist { + dict set nspathdict $pathns [dict create] ;#use consistent structure when nspathcommands false + } + } #set exportpatterns [nseval $location {::namespace export}] set allexported [list] set matched [list] @@ -933,7 +1022,7 @@ namespace eval punk::ns { #NOTE: info procs within namespace eval is different to 'info commands' within namespace eval (info procs doesn't look outside of namespace) set allprocs [namespace eval $location {::info procs}] #set allprocs [nseval $location {::info procs}] - set tails [lmap v $allchildren {nstail $v}] + set childtails [lmap v $allchildren {nstail $v}] set allaliases [list] set allensembles [list] set allooobjects [list] @@ -950,7 +1039,7 @@ namespace eval punk::ns { if {[string match *:: $a]} { #exception for alias such as ::p::2:: so that it doesn't show up as empty string #lappend aliases :: - #JMN - 2023 - better to dispaly an empty string somehow + #JMN - 2023 - better to display an empty string somehow lappend aliases "" } else { lappend aliases [nstail $a] @@ -993,8 +1082,9 @@ namespace eval punk::ns { } } if {$glob ne "*"} { - set tailmatches [lsearch -all -inline $tails $glob] - set fqchildren [lmap v $tailmatches {lindex ${location}::$v}] ;#lindex without indices is fast equivalent of 'val' + set childtailmatches [lsearch -all -inline $childtails $glob] + #set fqchildren [lmap v $childtailmatches {lindex ${location}::$v}] ;#lindex without indices is fast equivalent of 'val' or string cat + set exported [lsearch -all -inline $allexported $glob] set procs [lsearch -all -inline $allprocs $glob] #set aliases [lsearch -all -inline $allaliases $glob] @@ -1004,8 +1094,8 @@ namespace eval punk::ns { set imported [lsearch -all -inline $allimported $glob] set undetermined [lsearch -all -inline $allundetermined $glob] } else { - set tailmatches $tails - set fqchildren $allchildren + set childtailmatches $childtails + #set fqchildren $allchildren set exported $allexported set procs $allprocs #set aliases $allaliases @@ -1018,7 +1108,7 @@ namespace eval punk::ns { #itemcount will overcount if we are including commands as well as procs/exported etc - set itemcount 0 - incr itemcount [llength $tailmatches] + incr itemcount [llength $childtailmatches] incr itemcount [llength $commands] @@ -1032,8 +1122,9 @@ namespace eval punk::ns { #definitely don't count exportpatterns incr itemcount [llength $undetermined] - lappend nsdict_list [dict create\ - children [lsort $tailmatches]\ + + set nsdict [dict create\ + children [lsort $childtailmatches]\ commands $commands\ procs $procs\ exported $exported\ @@ -1045,9 +1136,11 @@ namespace eval punk::ns { namespacexport $exportpatterns\ undetermined $undetermined\ location $location\ + namespacepath $nspathdict\ glob $glob\ itemcount $itemcount\ ] + lappend nsdict_list $nsdict } return $nsdict_list #return [list children [lsort $tailmatches] commands $commands procs $procs exported $exported imported $imported aliases $aliases ensembles $ensembles ooobjects $ooobjects ooclasses $ooclasses namespacexport $exportpatterns location $location glob $glob] @@ -1056,8 +1149,7 @@ namespace eval punk::ns { #Must be no ansi when only single arg used. #review - ansi codes will be very confusing in some scenarios! #todo - only output color when requested (how?) or via repltelemetry ? - interp alias {} nscommands {} .= ,'ok'@0.= { - + interp alias {} nscommands2 {} .= ,'ok'@0.= { #Note: namespace argument to apply doesn't accept namespace segments with leading colon - so pipelines won't work fully in dodgily-named namespaces such as :::x #inspect -label namespace_current [namespace current] #inspect -label info_procs [info procs] @@ -1111,34 +1203,32 @@ namespace eval punk::ns { ::continue } - #NOTE - matched commands will return commands from global ns due to 'namespace eval' - #We don't simply do info commands ${base}::$what because it misses some oddly named things (JMN 2023 - like what?) - ::set matchedcommands [::pipeswitch { - #inspect '$ns' - #pipecase \ - # caseresult= $ns |input> \ - # 1.= {expr {[string length [string map [list * "" ? ""] $data]] == [string length $data]}} |> { info commands ${input}::* } - - #pipecase \ - # caseresult= $ns |input> { info commands ${input} } - - #::pipecase \ - # caseresult.= ::list $base $what |,basens/0,g/1> {::punk::ns::nseval $basens [::list ::info commands $g]} - ::pipecase \ - caseresult.= ::list $base $what |,basens/0,g/1> {namespace eval $basens [::list ::info commands $g]} - - }] - #lappend commandlist {*}[@@ok/result= $matchedcommands] - - #need to pull result from matchedcommands dict - #set cmd_tails [@@ok/result= $matchedcommands |> {::lmap v $data {punk::ns::nstail $v}}] - - ::set cmd_tails [::lmap v [::dict get $matchedcommands ok result] {::punk::ns::nstail $v}] - - ::set all_ns_tails [::lmap v $all_ns_commands {::punk::ns::nstail $v}] + if 0 { + #NOTE - matched commands will return commands from global ns due to 'namespace eval' - also any commands from namespaces in the 'namespace path' list + #We don't simply do info commands ${base}::$what because it misses some oddly named things (JMN 2023 - like what?) + #this was to support weird namespaces with leading/trailing colons - not an important usecase for the cost + ::set matchedcommands [::pipeswitch { + ::pipecase \ + caseresult.= ::list $base $what |,basens/0,g/1> {namespace eval $basens [::list ::info commands $g]} + }] + #lappend commandlist {*}[@@ok/result= $matchedcommands] + #need to pull result from matchedcommands dict + #set cmd_tails [@@ok/result= $matchedcommands |> {::lmap v $data {punk::ns::nstail $v}}] + ::set cmd_tails [::lmap v [::dict get $matchedcommands ok result] {::punk::ns::nstail $v}] + ::set all_ns_tails [::lmap v $all_ns_commands {::punk::ns::nstail $v}] + ::foreach c $cmd_tails { + ::if {$c in $all_ns_tails} { + ::if {$do_raw} { + ::lappend commandlist [::list $c $c] + } else { + ::lappend commandlist [::list $c $col[::list $c]$rst] + } + } + } + } else { - ::foreach c $cmd_tails { - ::if {$c in $all_ns_tails} { + ::set all_ns_tails [::lmap v $all_ns_commands {::punk::ns::nstail $v}] + foreach c $all_ns_tails { ::if {$do_raw} { ::lappend commandlist [::list $c $c] } else { @@ -1152,6 +1242,79 @@ namespace eval punk::ns { #we use the convention that a result of just \n represents a list of just the empty string - as we don't require duplicates anyway - so it shouldn't mean two empty strings. } |data@@ok/result> ::lsort -index 0 |> {::lmap v $data {::lindex $v 1}} |> {::if {![::llength $data]} {::return {}} else {::return [::join $data \n]\n}} = 0} { + ::set args [::lreplace $args $posn $posn] + ::set do_raw 1 + } + if {![llength $args]} { + lappend args * + } + ::foreach search $args { + ::if {$ci > [::llength $colors]-1} { + ::set ci 0 + } + ::if {$ci == 0 || $do_raw} { + ::set col "" + ::set rst "" + } else { + ::set col [a+ [::lindex $colors $ci] bold] + ::set rst [a+] + } + ::incr ci ;#colourindex + #inspect -label search $search + + ::if {![::llength $search]} { + ::set base $commandns + ::set what "*" + } else { + ::if {[::string match ::* $search]} { + ::set base [::punk::ns::nsprefix $search] + ::set what [::punk::ns::nstail $search] + } else { + ::set base $commandns + ::set what $search + } + } + + ::set all_ns_commands [::info commands [::punk::ns::nsjoin $base $what]] + #important not to call namespace eval (or punk::ns::nseval) on non-existant base - or it will be created + ::if {![::namespace exists $base]} { + ::continue + } + ::set all_ns_tails [::lmap v $all_ns_commands {::punk::ns::nstail $v}] + foreach c $all_ns_tails { + ::if {$do_raw} { + ::lappend commandlist [::list $c $c] + } else { + ::lappend commandlist [::list $c $col[::list $c]$rst] + } + } + } + set commandlist [lsort -index 0 $commandlist] + set results [list] + foreach pair $commandlist { + lappend results [lindex $pair 1] + } + #unless we always return trailing \n - it's impossible to represent a list containing only the empty string using \n (cannot disambiguate between empty list and/or list of 2 empty strings) + #we use the convention that a result of just \n represents a list of just the empty string - as we don't require duplicates anyway - so it shouldn't mean two empty strings. + if {![llength $results]} { + return {} + } else { + return [join $results \n]\n + } + } + interp alias {} nscommands {} punk::ns::nscommands + + + interp alias {} nscommands1 {} .= ,'ok'@0.= { set commandns [namespace current] #upvar caseresult caseresult @@ -1268,6 +1431,173 @@ namespace eval punk::ns { } + + namespace eval internal { + + + #maintenance: similar in punk::winrun + proc get_run_opts {options alias_dict arglist} { + if {[catch { + set callerinfo [info level -1] + } errM]} { + set caller "" + } else { + set caller [lindex $callerinfo 0] + } + + #update alias dict mapping shortnames to longnames - longnames to self + foreach o $options { + dict set alias_dict $o $o + } + set known_runopts [dict keys $alias_dict] + set runopts [list] + set cmdargs [list] + + set first_eopt_posn [lsearch $arglist --] + if {$first_eopt_posn >=0} { + set pre_eopts [lrange $arglist 0 $first_eopt_posn-1] + set is_eopt_for_runopts 1 ;#default assumption that it is for this function rather than part of user's commandline - cycle through previous args to disprove. + foreach pre $pre_eopts { + if {$pre ni $known_runopts} { + set is_eopt_for_runopts 0; #the first -- isn't for us. + } + } + } else { + set is_eopt_for_runopts 0 + } + #split on first -- if only known opts preceding (or nothing preceeding) - otherwise split on first arg that doesn't look like an option and bomb if unrecognised flags before it. + if {$is_eopt_for_runopts} { + set idx_first_cmdarg [expr $first_eopt_posn + 1] + set runopts [lrange $arglist 0 $idx_first_cmdarg-2] ;#exclude -- from runopts - it's just a separator. + } else { + set idx_first_cmdarg [lsearch -not $arglist "-*"] + set runopts [lrange $arglist 0 $idx_first_cmdarg-1] + } + set cmdargs [lrange $arglist $idx_first_cmdarg end] + foreach o $runopts { + if {$o ni $known_runopts} { + error "$caller: Unknown runoption $o - known options $known_runopts" + } + } + set runopts [lmap o $runopts {dict get $alias_dict $o}] + if {"-allowvars" in $runopts && "-disallowvars" in $runopts} { + puts stderr "Warning - conflicting options -allowvars & -disallowvars specified: $arglist" + } + + #maintain order: runopts $runopts cmdargs $cmdargs as first 4 args (don't break 'lassign [get_runopts $args] _ runopts _ cmdargs') + #todo - add new keys after these indicating type of commandline etc. + return [list runopts $runopts cmdargs $cmdargs] + } + + proc _pkguse_vars {varnames} { + while {"pkguse_vars_[incr n]" in $varnames} {} + return [concat $varnames pkguse_vars_$n] + } + proc tracehandler_nowrite {args} { + error "readonly in use block" + } + + } + + + #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}] + + + #todo support leading solo flags such as -capture to control whether we do a static capture of local vars in the ns + if {[string tolower $pkg_or_existing_ns] in [list :: global]} { + set ns :: + set ver "";# tcl version? + } else { + if {[string match ::* $pkg_or_existing_ns]} { + if {![namespace exists $pkg_or_existing_ns]} { + set ver [package require [string range $pkg_or_existing_ns 2 end]] + } else { + set ver "" + } + set ns $pkg_or_existing_ns + } else { + set ver [package require $pkg_or_existing_ns] + set ns ::$pkg_or_existing_ns + } + } + if {[namespace exists $ns]} { + if {[llength $cmdargs]} { + set binding {} + #if {[info level] == 1} { + # #up 1 is global + # set get_vars [list info vars] + #} else { + # set get_vars [list info locals] + #} + #set vars [uplevel 1 {*}$get_vars] + + #set vars [namespace eval $ns {info vars}] + + #review - upvar in apply within ns eval vs direct access of ${ns}::varname + set capture [namespace eval $ns { + apply { varnames { + while {"prev_args_[incr n]" in $varnames} {} + set capturevars [dict create] + set capturearrs [dict create] + foreach fullv $varnames { + set v [namespace tail $fullv] + upvar 1 $v var + if {$v eq "args"} { + dict set capturevars "prev_args$n" [list var $var] + } else { + if {(![array exists var])} { + dict set capturevars $v $var + } else { + dict set capturearrs $v [array get var] + } + } + } + return [dict create vars $capturevars arrs $capturearrs] + } } [info vars [namespace current]::*] ;#we are relying on info vars ::::* returning same as info vars ::* - a bit hacky (don't want to set any extra vars in the ns) + } ] + + + 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} + #tailcall apply [list args [string cat $scriptblock { {*}$args}] $ns] {*}$arglist + } + if {!$no_warnings & $use_vars} { + set script "" + foreach v [dict keys [dict get $capture vars]] { + append script [string map [list $v] { + trace add variable write ::punk::ns::internal::tracehandler_nowrite + #unset? + }] + } + append script \n $scriptblock + } else { + set script $scriptblock + } + if {$use_vars} { + tailcall apply [list [concat [dict keys [dict get $capture vars]] args] $script $ns] {*}[concat [dict values [dict get $capture vars]] $arglist] + } else { + tailcall apply [list args $scriptblock $ns] {*}$arglist + } + } else { + set out [punk::ns::ns/ / $ns] + append out \n $ver + return $out + } + } else { + error "Namespace $ns not found." + } + return $out + } + interp alias "" use "" punk::ns::pkguse + proc nsimport_noclobber {pattern {ns ""}} { set source_ns [namespace qualifiers $pattern] if {![namespace exists $source_ns]} { @@ -1307,6 +1637,8 @@ namespace eval punk::ns { return $imported_commands } + #todo - use ns::nsimport_noclobber instead + interp alias {} nsthis {} punk::ns::nspath_here_absolute interp alias {} nsorigin {} apply {ns {namespace origin [uplevel 1 ::punk::ns::nspath_here_absolute $ns]} ::} interp alias {} nsvars {} punk::ns::nsvars @@ -1320,16 +1652,21 @@ namespace eval punk::ns { interp alias {} nslist_dict {} punk::ns::nslist_dict #extra slash implies more verbosity (ie display commands instead of just nschildren) - interp alias {} :/ {} punk::ns::ns/ / - interp alias {} :// {} punk::ns::ns/ // interp alias {} n/ {} punk::ns::ns/ / interp alias {} n// {} punk::ns::ns/ // - interp alias {} ::/ {} punk::ns::nsup/ / - interp alias {} ::// {} punk::ns::nsup/ // + interp alias {} n/// {} punk::ns::ns/ /// + interp alias {} n/new {} punk::ns::n/new interp alias {} nn/ {} punk::ns::nsup/ / interp alias {} nn// {} punk::ns::nsup/ // + if 0 { + #we can't have ::/ without just plain / which is confusing. + interp alias {} :/ {} punk::ns::ns/ / + interp alias {} :// {} punk::ns::ns/ // interp alias {} :/new {} punk::ns::n/new - interp alias {} n/new {} punk::ns::n/new + interp alias {} ::/ {} punk::ns::nsup/ / + interp alias {} ::// {} punk::ns::nsup/ // + } + interp alias {} corp {} punk::ns::corp diff --git a/src/modules/punk/tcl-999999.0a1.0.tm b/src/modules/punk/tcl-999999.0a1.0.tm new file mode 100644 index 00000000..226ee25e --- /dev/null +++ b/src/modules/punk/tcl-999999.0a1.0.tm @@ -0,0 +1,51 @@ +# -*- 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 punk::tcl 999999.0a1.0 +# Meta platform tcl +# Meta license +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::tcl { + + + + +} + + + + + + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::tcl [namespace eval punk::tcl { + variable version + set version 999999.0a1.0 +}] +return \ No newline at end of file diff --git a/src/modules/punk/tcl-buildversion.txt b/src/modules/punk/tcl-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/punk/tcl-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/punk/unixywindows-999999.0a1.0.tm b/src/modules/punk/unixywindows-999999.0a1.0.tm index ff8532c0..8dbcf9fd 100644 --- a/src/modules/punk/unixywindows-999999.0a1.0.tm +++ b/src/modules/punk/unixywindows-999999.0a1.0.tm @@ -75,19 +75,19 @@ namespace eval punk::unixywindows { #as the tilde hasn't been normalized.. we can't assume we're running on the actual platform return ~/.. } - return [file dirname [winpath $path]] + return [file dirname [towinpath $path]] } #REVIEW high-coupling proc cdwin {path} { - set path [winpath $path] + set path [towinpath $path] if {$::repl::running} { repl::term::set_console_title $path } cd $path } proc cdwindir {path} { - set path [winpath $path] + set path [towinpath $path] if {$::repl::running} { repl::term::set_console_title $path } @@ -200,7 +200,7 @@ namespace eval punk::unixywindows { } #---------------------------------------------- - #leave the unixywindowws related aliases available on all platforms + #leave the unixywindows related aliases available on all platforms #interp alias {} cdwin {} punk::unixywindows::cdwin #interp alias {} cdwindir {} punk::unixywindoes::cdwindir #interp alias {} towinpath {} punk::unixywindows::towinpath diff --git a/src/modules/punk/winrun-999999.0a1.0.tm b/src/modules/punk/winrun-999999.0a1.0.tm new file mode 100644 index 00000000..10a70ed1 --- /dev/null +++ b/src/modules/punk/winrun-999999.0a1.0.tm @@ -0,0 +1,817 @@ +# -*- 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) J.M.Noble 2023 +# +# @@ Meta Begin +# Application punk::winrun 999999.0a1.0 +# Meta platform tcl +# Meta license BSD +# @@ Meta End + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Requirements +##e.g package require frobz +#package require twapi ;#not loaded here because as of 2023-11 load is *very* slow. Todo - query APN re return to faster partial loading facility for twapi subsets. +#slow twapi load at startup can be ameliorated by async loading the dll in another thread in circumstances where it's not needed immediately anyway - but this doesn't help for filters where we need twapi functionality asap. + +#see also: https://daviddeley.com/autohotkey/parameters/parameters.htm#WINNOSTANDARD +#https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way + +#see also: Tip 424: Improving [exec] +#https://core.tcl-lang.org/tips/doc/trunk/tip/424.md + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +namespace eval punk::winrun { + namespace export * + proc twapi_exec {cmdline args} { + package require twapi + set psinfo [twapi::create_process {} -cmdline $cmdline {*}$args] + } + proc tw_run {cmdline} { + #twapi::create_file to redirect? + package require twapi + set psinfo [twapi::create_process {} -cmdline $cmdline -returnhandles 1] + lassign $psinfo _pid _tid hpid htid + set waitresult [twapi::wait_on_handle $hpid -wait -1] + #e.g timeout, signalled + if {$waitresult eq "timeout"} { + puts stderr "tw_run: timeout waiting for process" + } + set code [twapi::get_process_exit_code $hpid] + twapi::close_handle $htid + twapi::close_handle $hpid + return [dict create exitcode $code] + } + + #completely raw to windows createprocess API - caller will really need to understand what they're doing. + proc runraw {args} { + foreach w $args { + append cmdline $w " " + } + set cmdline [string range $cmdline 0 end-1] + #puts stdout --$cmdline + tw_run $cmdline + } + + + #apparently there is no windows API function to do the reverse of CommandLineToArgvW + proc quote_win {args} { + #The algorithm used here is that shown in ArgvQuote from the following article: + #https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way + # -- --- ---- --- --- --- --- --- --- --- --- + set splitargs [internal::get_run_opts $args] + set runopts [dict get $splitargs runopts] + set cmdargs [dict get $splitargs cmdargs] + set quiet 0 + if {"-quiet" in $runopts} { + set quiet 1 + } + set verbose 0 + if {"-verbose" in $runopts} { + set verbose 1 + } + # -- --- ---- --- --- --- --- --- --- --- --- + + + set raw_cmdline "" + set tcl_list [list] + set i 0 + foreach a $cmdargs { + set copy [internal::objclone $a] + append raw_cmdline "$copy " + lappend tcl_list $copy + if {$i == 0 && !$quiet} { + if {"[string index $copy 0][string index $copy end]" eq {""}} { + #review legit reasons to call with quoted first arg. Such users can use the -q flag so that this warning can remain to help in general debugging + puts stderr "WARNING: quote_win first argument should not be pre-quoted if it is to be interpreted correctly on windows (e.g with CommandLineToArgvW)" + } + } + incr i + } + if {[llength $cmdargs] > 0} { + set raw_cmdline [string range $raw_cmdline 0 end-1] ;#trim 1 trailing space + } + if {$verbose} { + puts stdout "==raw_cmdline== $raw_cmdline" ;# string built from list elements is different to string rep of original list which potentially has Tcl escapes visible + } + + #don't run regexp on the list rep + #set wordranges [regexp -inline -all -indices {\S+} $raw_cmdline] + #set raw_parts [list] + #foreach range $wordranges { + # set word [string range $raw_cmdline {*}$range] + # lappend raw_parts [internal::objclone $word] + #} + + + set cmdline "" + set i 0 + foreach w $tcl_list { + #puts "== processing word $w" + if {$w ne "" && [string first " " $w] < 0 && [string first \t $w] < 0 && [string first \n $w] < 0 && [string first {"} $w] < 0 && [string first \v $w] < 0} { + append cmdline "$w " + continue + } + append cmdline {"} + set chars [split $w ""] + set wordlen [string length $w] + set nlast [expr {$wordlen -1}] + for {set n 0} {$n<$wordlen} {incr n} { + set char [lindex $chars $n] + set num_backslashes 0 + while {$char eq "\\" && $n<$nlast} { + incr num_backslashes + incr n + set char [lindex $chars $n] + } + if {$n > $nlast} { + append cmdline [string repeat "\\" [expr {$num_backslashes * 2}]] + break + } elseif {$char eq {"}} { + #escape all backslashes and the following double-quote + append cmdline [string repeat "\\" [expr {$num_backslashes * 2 + 1}]] $char + } else { + append cmdline [string repeat "\\" $num_backslashes] $char + } + + } + append cmdline {" } + incr i + } + set cmdline [string range $cmdline 0 end-1] + # ----------------- + if {$verbose} { + puts stdout --cmdline->$cmdline + } + # ----------------- + #tw_run $cmdline + #assert - can be treated as tcl list ? + return $cmdline + } + interp alias "" [namespace current]::quote_wintcl "" ::punk::winrun::quote_win ;#just for symmetry with unquote_wintcl + + proc unquote_win {standard_quoted_cmdline} { + #This twapi call uses the windows api function: CommandLineToArgvW (Twapi_CommandLineToArgv calls it and handles the winchars conversion) + # - a quoted first word such as the following will not turn out well: "\"cmd\"" + # - First word on commandline is expected to be the program name - and not wrapped in extra double quotes even if it contains spaces. + twapi::get_command_line_args $standard_quoted_cmdline + } + + #equivalent of unquote_win implemented in Tcl - for testing if assumptions are correct, and whether the api does something different between os versions. + #There are differences in particular with repeated double quotes. + #This function seems to behave in alignment with how tclsh gets it's argv parameters - whereas Twapi 4.7.2 CommandLineToArgvW splits differently + #e.g for commandline: cmd """a b c""" etc + #unquote_wintcl and tclsh ::argv give 2 args, "a b c" , etc + #CommandLineToArgvW gives 4 args "a , b , c" , etc + # + proc unquote_wintcl {standard_quoted_cmdline} { + #with reference to https://daviddeley.com/autohotkey/parameters/parameters.htm post2008 ms C/C++ commandline parameter parsing algorithm (section 5.10) + set paramlist [list] + set remainder $standard_quoted_cmdline + set lastremlen [string length $standard_quoted_cmdline] + #note 1st arg (program name) - anything up to first whitespace or anything within first 2 double-quotes encountered - so escaped doublequotes can't be part of first word. + while {[string length $remainder]} { + if {[llength $paramlist] == 0} { + set pinfo [get_firstparam_wintcl $remainder] + } else { + set pinfo [get_nextparam_wintcl $remainder] + } + if {[dict get $pinfo status] ne "ok"} { + puts stderr "paramlist so far: '$paramlist'" + error "unquote_wintcl error [dict get $pinfo status]" + } + lappend paramlist [dict get $pinfo param] + set remainder [dict get $pinfo remainder] + set remainder [string trimleft $remainder " \t"] + set remlen [string length $remainder] + if {$remlen && ($remlen >= $lastremlen)} { + #sanity check + error "unquote_wintcl failed to progress in parsing cmdline $standard_quoted_cmdline - stuck with remainder $remlen" + } + set lastremlen $remlen + } + return $paramlist + } + + #get 'program name' first word under different rules to subsequent arguments in the cmdline + proc get_firstparam_wintcl {cmdline} { + set in_doublequote_part 0 + set chars [split $cmdline ""] + set chunklen [llength $chars] + set n 0 + set p "" + if {[lindex $chars 0] eq {"}} { + set in_doublequote_part 1 + } else { + append p [lindex $chars 0] + } + incr n + + while {$n<$chunklen && ($in_doublequote_part || ([lindex $chars $n] ni [list " " \t]))} { + if {[lindex $chars $n] eq {"}} { + break + } + append p [lindex $chars $n] + incr n + } + set rem [string range $cmdline $n+1 end] + #puts "----p>$p<------r>$rem<-----" + return [dict create status "ok" param $p remainder $rem] + } + + #non first-word parsing. + proc get_nextparam_wintcl {cmdline} { + #post 2008 windows double-quote handling system. + set chars [split $cmdline ""] + set chunklen [llength $chars] + set status "parsing" + set p "" + set in_doublequote_part 0 + #allow n to go 1 above highest index in $chars for this algorithm + for {set n 0} {$n<=$chunklen} {incr n} { + set copychar true + set num_backslashes 0 + while {[lindex $chars $n] eq "\\"} { + incr num_backslashes + incr n + } + if {[lindex $chars $n] eq {"}} { + if {$num_backslashes % 2 == 0} { + #even + if {$in_doublequote_part} { + if {[lindex $chars $n+1] eq {"}} { + incr n ;#move to second {"} + } else { + set copychar false + set in_doublequote_part 0 + } + } else { + set copychar false + set in_doublequote_part 1 + } + } + #whether odd or even, dividing by 2 does what we need + set num_backslashes [expr {$num_backslashes / 2}] + } + append p [string repeat "\\" $num_backslashes] + if {$n == $chunklen || (!$in_doublequote_part && [lindex $chars $n] in [list " " \t])} { + set status "ok" + break + } + if {$copychar} { + append p [lindex $chars $n] + } + } + set rem [string range $cmdline $n+1 end] + #puts "----p>$p<------r>$rem<-----" + return [dict create status $status param $p remainder $rem] + } + + proc runwin {args} { + tw_run [quote_win {*}$args] + } + + #an experiment - this is essentially an identity transform unless flags are set. - result afer cmd.exe processes escapes is the same as running raw with no quoting + #this follows the advice of 'don't let cmd see any double quotes unescaped' - but that's effectively a pretty useless strategy. + #The -useprequoted and -usepreescaped flags are the only difference + #these rely on the fact we can prepend a caret to each argument without affecting the resulting string - and use that as an indicator to treat specific input 'arguments' differently i.e by keeping existing escapes only. + proc quote_cmd {args} { + lassign [internal::get_run_opts $args] _r runopts _c cmdargs + set use_prequoted [expr {"-useprequoted" in $runopts}] + set use_preescaped [expr {"-usepreescaped" in $runopts}] + set verbose [expr {"-verbose" in $runopts}] + #As this quoting scheme allows & > < etc to execute depending on quote state - it doesn't make sense to default to blocking %var% or !var! here. + set disallowvars [expr {"-disallowvars" in $runopts}] + + if {![llength $cmdargs]} { + return "Usage: quote_cmd ?runopt? ... ?--? ?cmd? ?cmdarg? ..." + } + foreach a $cmdargs { + set copy [internal::objclone $a] + append raw_cmdline "$copy " + lappend tcl_list $copy + } + + set cmdline "" + set i 0 + set meta_chars [list {"} "(" ")" ^ < > & |] + #note that %var% and !var! work the same whether within a double quote section or not + if {$disallowvars} { + lappend meta_chars % ! + } + + #unbalanced quotes in %varname% will affect output - but aren't seen by this parser - which means they will only result in double escaping - not exiting escape mode. (this is good) + #!varname! with delayed expansion (cmd.exe /v /c ...) seems to be safer as it doesn't appear to allow breakage of quoting + set cmd_in_quotes 0 + #todo - transition of cmd_in_quotes from 0 -> 1 only is affected by number of carets preceding quote! + foreach w $tcl_list { + set qword "" + set wordlen [string length $w] + set nlast [expr {$wordlen -1}] + set chars [split $w ""] + set wordlen [string length $w] + set nlast [expr {$wordlen -1}] + + if {$use_prequoted} { + if {[string range $w 0 1] eq {^"}} { + #pass entire argument (less leading caret) through with existing quoting - no adjustment to cmd_in_quotes state. + append cmdline [string range $w 1 end] " " + continue + } + } + if {$use_preescaped} { + if {[string index $w 0] eq {^}} { + #pass entire argument (less leading caret) through with existing quoting - no adjustment to cmd_in_quotes state. + append cmdline [string range $w 1 end] " " + continue + } + } + + + for {set n 0} {$n<$wordlen} {incr n} { + set char [lindex $chars $n] + set num_carets 0 + while {$char eq "^" && $n<$nlast} { + incr num_carets + incr n + set char [lindex $chars $n] + } + + if {$char eq {"}} { + if {$cmd_in_quotes} { + append qword [string repeat "^" [expr {$num_carets *2 + 1}]] {"} + set cmd_in_quotes [expr {!$cmd_in_quotes}] + } else { + #cmd.exe echo behaviour: + # ^" -> " + # ^^" -> ^" + # ^^^" -> ^" + # ^^^^" -> ^^" + if {$num_carets % 2} { + set cmd_in_quotes 0 ;#odd number of preceding carets make this dquote a literal stay out of quotes mode + append qword [string repeat "^" [expr {$num_carets}]] {"} ;# + } else { + set cmd_in_quotes 1; #carets all refer to each other - quote is uncareted. + append qword [string repeat "^" [expr {$num_carets + 1}]] {"} ;# + } + } + #set cmd_in_quotes [expr {!$cmd_in_quotes}] + } else { + if {$cmd_in_quotes} { + if {$char in $meta_chars} { + append qword [string repeat "^" [expr {$num_carets *2 + 1}]] $char ;# + } else { + append qword [string repeat "^" [expr {$num_carets *2}]] $char ;# + } + } else { + if {$char in $meta_chars} { + append qword [string repeat "^" [expr {$num_carets}]] $char + } else { + append qword [string repeat "^" [expr {$num_carets}]] $char + } + } + } + } + + append cmdline $qword " " + incr i + } + set cmdline [string range $cmdline 0 end-1] + if {$verbose} { + puts stdout --cmdline->$cmdline + } + return $cmdline + } + + #This does what Sebres has implemented for Tcl's exec already - pass through that works for non builtins that are run via cmd.exe and require standard argv parsing + # + #tracked blocking of vars - after winquoting when in quotes,prefix % with (unslashed) quote - when outside quotes - prefix with ^ + #(always using unslashed quotes considered - seems more likely to cause prolems with the argv parsing) + # ! can't be blocked with carets ... always use quotes + #other cmd specials - block only outside of quotes + #existing carets? + #note that /v changes the way carets go through - we need twice as many ^ when /v in place e.g x^^^^y to get x^y vs x^^y to get x^y when /v not present - review - can we sensibly detect /v? + #don't caret quotes. + proc quote_cmdpassthru {args} { + lassign [internal::get_run_opts $args] _r runopts _c cmdargs + set allowvars [expr {"-allowvars" in $runopts}] + set verbose [expr {"-verbose" in $runopts}] + #review - may need to force quoting of barewords by quote_win to ensure proper behaviour if bareword contains cmd specials? + #?always treatable as a list? review + set tcl_list [lmap v $cmdargs {internal::objclone $v}] + set meta_chars [list {<} {>} & |] ;#caret-quote when outside of cmd.exe's idea of a quoted section - carets will disappear from passed on string + set cmdline "" + set in_quotes 0 + foreach w $tcl_list { + set winquoted [quote_win x $w] ;#pass bogus app-name as first word - as first word subject to different rules + set chars [split [string range $winquoted 2 end] ""] ;# strip bogus before splitting + set had_quotes 0 + if {{"} in $chars} { + set had_quotes 1 + } + set wordlen [llength $chars] + #set nlast [expr {$wordlen -1}] + set qword "" + for {set n 0} {$n<$wordlen} {incr n} { + set num_slashes 0 + if {[lindex $chars $n] eq {"}} { + set in_quotes [expr {!$in_quotes}] + append qword {"} + } elseif {[lindex $chars $n] in [list "%"]} { + if {$allowvars} { + set tail [lrange $chars $n+1 end] + #?? + } + #if %var% was in original string - a variable named %"var"% can be substituted after we have done our quoting. + #no matter what quoting scheme we use - there will be a corresponding string between %'s that can in theory be exploited if + if {$in_quotes} { + #note that the *intended* quoting will be opposite to the resultant quoting from wrapping with quote_win + #therefore, counterintuitively we can enable the var when in_quotes is true here, and &cmd won't run. + #double quotes in the var don't seem to cause cmd.exe to change it's concept of in_quotes so &cmd also won't run + #However.. backspace can can break quoting. e.g \b&cmd + if {$allowvars} { + append qword [lindex $chars $n] + } else { + append qword {"} [lindex $chars $n] {"} ;#add in pairs so we don't disturb structure for argv + } + } else { + #allow vars here is also dangerous we need to lookahead and scan the value and quote accordingly + if {$allowvars} { + + append qword [lindex $chars $n] + } else { + append qword {^} [lindex $chars $n] + } + } + } elseif {[lindex $chars $n] eq "!"} { + if {$allowvars} { + append qword "!" + } else { + append qword {"} {!} {"} + } + } elseif {[lindex $chars $n] eq "^"} { + if {$in_quotes} { + append qword {"^^"} ;#add quotes in pairs so we don't disturb structure for argv + } else { + append qword {^^} + } + } else { + if {[lindex $chars $n] in $meta_chars} { + if {$in_quotes} { + append qword [lindex $chars $n] + } else { + append qword "^" [lindex $chars $n] + } + } else { + append qword [lindex $chars $n] + } + } + } + append cmdline $qword " " + + } + set cmdline [string range $cmdline 0 end-1] + if {$verbose} { + puts stdout --cmdline->$cmdline + } + return $cmdline + } + # - This approach with repeated double quotes gives inconsistent behaviour between twapi CommandLineToArgvW and tclsh - + #prepare arguments that are given to cmd.exe such that they will be passed through to an executable that uses standard windows commandline parsing such as CommandLineToArgvW + #for each arg: + #double up any backslashes that precede double quotes, double up existing double quotes - then wrap in a single set of double quotes if argument had any quotes in it. + #This doesn't use \" or ^ style escaping - but the 2008+ argv processing on windows supposedly does what we want with doubled-up quotes and slashes, and cmd.exe passes them through + #In practice - it seems less consistent/reliable + proc quote_cmdpassthru_test {args} { + lassign [internal::get_run_opts $args] _r runopts _c cmdargs + set allowvars [expr {"-allowvars" in $runopts}] + set verbose [expr {"-verbose" in $runopts}] + + set tcl_list [lmap v $cmdargs {internal::objclone $v}] + set meta_chars [list {"} "(" ")" ^ < > & |] + if {!$allowvars} { + lappend meta_chars % ! + } + set cmdline "" + foreach w $tcl_list { + set chars [split $w ""] + set wordlen [llength $chars] + #set nlast [expr {$wordlen -1}] + set qword "" + for {set n 0} {$n<$wordlen} {incr n} { + set num_slashes 0 + while {[lindex $chars $n] eq "\\" && $n<$wordlen} { + incr num_slashes + incr n + } + if {[lindex $chars $n] eq {"}} { + append qword [string repeat "\\" [expr {$num_slashes *2}]] {""} ;#double up both + } else { + #don't double up slashes if not followed by dquote + append qword [string repeat "\\" $num_slashes] [lindex $chars $n] + } + } + if {[string first {"} $qword] >=0} { + append cmdline {"} $qword {"} " " + } else { + append cmdline $qword " " + } + } + set cmdline [string range $cmdline 0 end-1] + if {$verbose} { + puts stdout --cmdline->$cmdline + } + return $cmdline + } + + #caret quoting of all meta_chars + proc quote_cmdblock {args} { + lassign [internal::get_run_opts $args] _r runopts _c cmdargs + set allowvars [expr {"-allowvars" in $runopts}] + set allowquotes [expr {"-allowquotes" in $runopts}] + set verbose [expr {"-verbose" in $runopts}] + set tcl_list [lmap v $cmdargs {internal::objclone $v}] + set cmdline "" + set i 0 + set meta_chars [list "(" ")" ^ < > & |] + if {!$allowvars} { + lappend meta_chars % ! + } + if {!$allowquotes} { + lappend meta_chars {"} + } + foreach w $tcl_list { + set wordlen [string length $w] + set nlast [expr {$wordlen -1}] + set chars [split $w ""] + foreach char $chars { + if {$char in $meta_chars} { + append cmdline "^$char" + } else { + append cmdline $char + } + } + append cmdline " " + incr i + } + set cmdline [string range $cmdline 0 end-1] + if {$verbose} { + puts stdout --cmdline->$cmdline + } + return $cmdline + } + + proc quote_cmd2 {args} { + set cmdargs $args + set tcl_list [lmap v $cmdargs {internal::objclone $v}] + + set cmdline "" + set i 0 + set meta_chars [list {"} "(" ")" ^ < > & |] ;#deliberately don't include % - it should work quoted or not. + #unbalanced quotes in %varname% will affect output - but aren't seen by this parser - which means they will only result in double escaping - not exiting escape mode. (this is good) + set cmd_in_quotes 0 + foreach w $tcl_list { + set wordlen [string length $w] + set nlast [expr {$wordlen -1}] + set chars [split $w ""] + foreach char $chars { + if {$char eq {"}} { + append cmdline {^"} + set cmd_in_quotes [expr {!$cmd_in_quotes}] + } else { + if {$cmd_in_quotes} { + if {$char in $meta_chars} { + append cmdline "^$char" + } else { + append cmdline $char + } + } else { + append cmdline $char + } + } + } + append cmdline " " + incr i + } + set cmdline [string range $cmdline 0 end-1] + puts stdout --cmdline->$cmdline + return $cmdline + } + + proc runcmd {args} { + set cmdline [quote_cmd {*}$args] + tw_run $cmdline + } + proc runcmdpassthru {args} { + set cmdline [quote_cmdpassthru {*}$args] + tw_run $cmdline + } + proc runcmdblock {args} { + set cmdline [quote_cmdblock {*}$args] + tw_run $cmdline + } + + + #round-trip test + #use standard(!) win arg quoting first - then deconstruct using the win32 api, and the tcl implementation + proc testrawline {rawcmdline} { + puts "input string : $rawcmdline" + set win_argv [unquote_win $rawcmdline] + puts "unquote_win CommandLineToArgvW : $win_argv" + set wintcl_argv [unquote_wintcl $rawcmdline] + puts "unquote_wintcl : $wintcl_argv" + return $win_argv + } + proc testlineargs {args} { + puts "input list : $args" + puts " argument count : [llength $args]" + puts "input string : [join $args " "]" + puts [string repeat - 20] + set standard_escape_line [quote_win {*}$args] + set argv_from_win32 [unquote_win $standard_escape_line] + puts "quote_win win : $standard_escape_line" + puts "unquote_win CommandLineToArgvW : $argv_from_win32" + puts " argument count : [llength $argv_from_win32]" + #so far - gives same output as windows api - this may vary by os version? + #set argv_from_wintcl [unquote_wintcl $standard_escape_line] + #puts "unquote_wintcl tcl implementation : $argv_from_win32" + puts [string repeat - 20] + + puts "quote_cmd cmd.exe style quoting : [quote_cmd {*}$args]" + puts [string repeat - 20] + + set cline_blocked [quote_cmdblock {*}$args] + set cline_blocked_argv [unquote_win $cline_blocked] + puts "quote_cmdblock cmd.exe protect : $cline_blocked" + puts "unquote_win CommandLineToArgvW : $cline_blocked_argv" + puts " argument count : [llength $cline_blocked_argv]" + puts [string repeat - 20] + + set cline_passthru [quote_cmdpassthru {*}$args] + set cline_passthru_argv [unquote_win $cline_passthru] + puts "quote_cmdpassthru to argv parser : $cline_passthru" + puts "unquote_win CommandLineToArgvW : $cline_passthru_argv" + puts " argument count : [llength $cline_passthru_argv]" + puts [string repeat - 20] + + #if {[file exists [file dirname [info nameofexecutable]]/../scriptlib/showargs.tcl]} { + # runraw tclsh showargs.tcl {*}$cline_blocked + #} + return $argv_from_win32 + } + + proc import {pattern {ns ""}} { + set pattern ::punk::winrun::$pattern + if {$ns eq ""} { + set ns [uplevel 1 {namespace current}] + } + internal::nsimport_noclobber $pattern $ns + } + + namespace eval internal { + # -- --- --- + #get a copy of the item without affecting internal rep + #this isn't critical for most usecases - but can be of use for example when trying not to shimmer path objects to strings (filesystem performance impact in some cases) + proc objclone {obj} { + append obj2 $obj {} + } + # -- --- --- + + #get_run_opts - allow completely arbitrary commandline following controlling flags - with no collision issues if end-of-opts flag "--" is used. + #singleton flags allowed preceding commandline. (no support for option-value pairs in the controlling flags) + #This precludes use of executable beginning with a dash unless -- provided as first argument or with only known run-opts preceding it. + #This should allow any first word for commandlist even -- itself if a protective -- provided at end of any arguments intended to control the function. + proc get_run_opts {arglist} { + if {[catch { + set callerinfo [info level -1] + } errM]} { + set caller "" + } else { + set caller [lindex $callerinfo 0] + } + + #we provide -nonewline even for 'run' even though run doesn't deliver stderr or stdout to the tcl return value + set options [list "-allowvars" "-allowquotes" "-disallowvars" "-useprequoted" "-usepreescaped" "-quiet" "-verbose" "-verbose2" "-echo" "-nonewline"] + set aliases [dict create\ + -av -allowvars\ + -dv -disallowvars\ + -aq -allowquotes\ + -up -useprequoted\ + -ue -usepreescaped\ + -q -quiet\ + -v -verbose\ + -vv -verbose2\ + -e -echo\ + -n -nonewline\ + ] + #build alias dict mapping shortnames to longnames - longnames to self + set alias_dict $aliases + foreach o $options { + dict set alias_dict $o $o + } + set known_runopts [dict keys $alias_dict] + set runopts [list] + set cmdargs [list] + + set first_eopt_posn [lsearch $arglist --] + if {$first_eopt_posn >=0} { + set pre_eopts [lrange $arglist 0 $first_eopt_posn-1] + set is_eopt_for_runopts 1 ;#default assumption that it is for this function rather than part of user's commandline - cycle through previous args to disprove. + foreach pre $pre_eopts { + if {$pre ni $known_runopts} { + set is_eopt_for_runopts 0; #the first -- isn't for us. + } + } + } else { + set is_eopt_for_runopts 0 + } + #split on first -- if only known opts preceding (or nothing preceeding) - otherwise split on first arg that doesn't look like an option and bomb if unrecognised flags before it. + if {$is_eopt_for_runopts} { + set idx_first_cmdarg [expr $first_eopt_posn + 1] + set runopts [lrange $arglist 0 $idx_first_cmdarg-2] ;#exclude -- from runopts - it's just a separator. + } else { + set idx_first_cmdarg [lsearch -not $arglist "-*"] + set runopts [lrange $arglist 0 $idx_first_cmdarg-1] + } + set cmdargs [lrange $arglist $idx_first_cmdarg end] + foreach o $runopts { + if {$o ni $known_runopts} { + error "$caller: Unknown runoption $o - known options $known_runopts" + } + } + set runopts [lmap o $runopts {dict get $alias_dict $o}] + if {"-allowvars" in $runopts && "-disallowvars" in $runopts} { + puts stderr "Warning - conflicting options -allowvars & -disallowvars specified: $arglist" + } + + #maintain order: runopts $runopts cmdargs $cmdargs as first 4 args (don't break 'lassign [get_runopts $args] _ runopts _ cmdargs') + #todo - add new keys after these indicating type of commandline etc. + return [list runopts $runopts cmdargs $cmdargs] + } + + #maintenance: home is punk::ns package + proc nsimport_noclobber {pattern {ns ""}} { + set source_ns [namespace qualifiers $pattern] + if {![namespace exists $source_ns]} { + error "nsimport_noclobber error namespace $source_ns not found" + } + if {$ns eq ""} { + set ns [uplevel 1 {namespace current}] + } elseif {![string match ::* $ns]} { + set nscaller [uplevel 1 {namespace current}] + set ns [punk::nsjoin $nscaller $ns] + } + set a_export_patterns [namespace eval $source_ns {namespace export}] + set a_commands [info commands $pattern] + set a_tails [lmap v $a_commands {namespace tail $v}] + set a_exported_tails [list] + foreach pattern $a_export_patterns { + set matches [lsearch -all -inline $a_tails $pattern] + foreach m $matches { + if {$m ni $a_exported_tails} { + lappend a_exported_tails $m + } + } + } + set imported_commands [list] + foreach e $a_exported_tails { + set imported [namespace eval $ns [string map [list $e $source_ns] { + set cmd "" + if {![catch {namespace import ::}]} { + set cmd + } + set cmd + }]] + if {[string length $imported]} { + lappend imported_commands $imported + } + } + return $imported_commands + } + } ;# end ns internal + #comment out for manual import + import * :: +} + + + + + + + + + +# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +## Ready +package provide punk::winrun [namespace eval punk::winrun { + variable version + set version 999999.0a1.0 +}] +return \ No newline at end of file diff --git a/src/modules/punk/winrun-buildversion.txt b/src/modules/punk/winrun-buildversion.txt new file mode 100644 index 00000000..f47d01c8 --- /dev/null +++ b/src/modules/punk/winrun-buildversion.txt @@ -0,0 +1,3 @@ +0.1.0 +#First line must be a semantic version number +#all other lines are ignored. diff --git a/src/modules/shellfilter-0.1.8.tm b/src/modules/shellfilter-0.1.8.tm index 54110d4c..53abd15c 100644 --- a/src/modules/shellfilter-0.1.8.tm +++ b/src/modules/shellfilter-0.1.8.tm @@ -1512,7 +1512,13 @@ namespace eval shellfilter { set i 0 set info [dict create] set testlist [list] - foreach item $inputlist { + 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 @@ -1944,6 +1950,11 @@ namespace eval shellfilter { } } + 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 diff --git a/src/modules/shellrun-0.1.tm b/src/modules/shellrun-0.1.tm index f242ab74..5988ec40 100644 --- a/src/modules/shellrun-0.1.tm +++ b/src/modules/shellrun-0.1.tm @@ -68,7 +68,8 @@ namespace eval shellrun { - + #maintenance: similar used in punk::ns & punk::winrun + #todo - take runopts + aliases as args proc get_run_opts {arglist} { if {[catch { set callerinfo [info level -1] @@ -96,68 +97,7 @@ namespace eval shellrun { return [list runopts $runopts cmdargs $cmdargs] } - #2023 - a more serious attempt at proper raw argument passing to windows - proc trun {args} { - #puts stdout --rawargs-list--$args - set_last_run_display [list] - set splitargs [get_run_opts $args] - set runopts [dict get $splitargs runopts] - set cmdargs [dict get $splitargs cmdargs] - - if {"-nonewline" in $runopts} { - set nonewline 1 - } else { - set nonewline 0 - } - set cmdline "" - foreach a $args { - append cmdline "[punk::objclone $a] " - } - if {[llength $args] > 0} { - set cmdline [string range $cmdline 0 end-1] ;#trim 1 trailing space - } - #puts stdout "==args_as_string==$cmdline" - - - #don't run regexp on the list rep - set wordranges [regexp -inline -all -indices {\S+} $cmdline] - #set wordparts [list] - set cmdlineparts [list] - foreach range $wordranges { - set word [string range $cmdline {*}$range] - lappend cmdlineparts [punk::objclone $word] - #lappend wordparts [punk::objclone $word] - } - - #puts stdout -->$wordparts - #set listinfo [shellfilter::list_element_info $wordparts] - #puts stdout --$listinfo - set cmdline "" - set i 0 - #----------- - #the list_element_info analysys left in for debugging in case the quoting/internal rep mechanism in Tcl changes from version to version - #we hopefully don't need to check the "wouldescape" "wouldescape" member of listinfo if we've been careful enough - #---------- - foreach w $cmdlineparts { - #set wordinfo [dict get $listinfo $i] - #if {[dict get $wordinfo wouldbrace] && [dict get $wordinfo head_tail_names] eq {lbrace rbrace}} { - # append cmdline [string range $w 1 end-1] - #} else { - append cmdline $w - #} - append cmdline " " - incr i - } - # ----------------- - #puts stdout --1>$cmdline - #All this mucking about is because: - #cmdline could be something like: cmd /c echo x\"x^b - #when represented as a list - but we just want it as: cmd /c echo x"x^b - # ----------------- - set cmdline [string range $cmdline 0 end-1] - shellrun::tw_run $cmdline - } proc run {args} { set_last_run_display [list] @@ -732,27 +672,7 @@ namespace eval shellrun { } namespace eval shellrun { - proc twapi_exec {cmdline args} { - package require twapi - set psinfo [twapi::create_process {} -cmdline $cmdline {*}$args] - } - proc tw_run {cmdline} { - #twapi::create_file to redirect? - package require twapi - set psinfo [twapi::create_process {} -cmdline $cmdline -returnhandles 1] - lassign $psinfo _pid _tid hpid htid - set waitresult [twapi::wait_on_handle $hpid -wait -1] - #e.g timeout, signalled - if {$waitresult eq "timeout"} { - puts stderr "tw_run: timeout waiting for process" - } - set code [twapi::get_process_exit_code $hpid] - twapi::close_handle $htid - twapi::close_handle $hpid - return [dict create exitcode $code] - } interp alias {} run {} shellrun::run - interp alias {} trun {} shellrun::trun interp alias {} sh_run {} shellrun::sh_run interp alias {} runout {} shellrun::runout interp alias {} sh_runout {} shellrun::sh_runout @@ -762,7 +682,6 @@ namespace eval shellrun { interp alias {} sh_runx {} shellrun::sh_runx interp alias {} runraw {} shellrun::runraw - interp alias {} twexec {} shellrun::twapi_exec #the shortened versions deliberately don't get pretty output from the repl diff --git a/src/scriptapps/punk87.cmd b/src/scriptapps/punk87.cmd new file mode 100644 index 00000000..c68752d9 --- /dev/null +++ b/src/scriptapps/punk87.cmd @@ -0,0 +1,286 @@ +set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide s {proc $s args {}}; Hide :;rename set s2;Hide set;s2 1 list]"; set -- : "$@"; $1 = @' +: heredoc1 - hide from powershell (close sqote for unix shells) ' \ +: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \ +: "[Hide @ECHO; Hide ); Hide (;Hide echo]#not necessary but can help avoid errs in testing" +: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ +@REM { +@REM DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required and will reduce functionality. +@REM Even comment lines can be part of the functionality of this script - modify with care. +@REM Change the value of nextshell in the next line if desired, and code within payload sections as appropriate. +@SET "nextshell=pwsh" +@REM nextshell set to pwsh,sh,bash or tclsh +@REM @ECHO nextshell is %nextshell% +@SET "validshells=pwsh,sh,bash,tclsh" +@CALL SET keyRemoved=%%validshells:%nextshell%=%% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix) +@REM -- This section intended only to launch the next shell +@REM -- Avoid customising this if possible. cmd/batch script is probably the least expressive language. +@REM -- custom windows payloads should be in powershell,tclsh or sh/bash code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SETLOCAL EnableExtensions EnableDelayedExpansion +@SET "winpath=%~dp0" +@SET "fname=%~nx0" +@REM @ECHO fname %fname% +@REM @ECHO winpath %winpath% +@IF %nextshell%==pwsh ( + CALL pwsh -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned + COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL + REM test availability of preferred option of powershell7+ pwsh + CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + IF !pwshtest_exitcode!==0 CALL pwsh -nop -nol "%~dp0%~n0.ps1" %* & SET task_exitcode=!errorlevel! + REM fallback to powershell if pwsh failed + IF NOT !pwshtest_exitcode!==0 ( + REM CALL powershell -nop -nol -c write-host powershell-found + CALL powershell -nop -nol -file "%~dp0%~n0.ps1" %* + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF %nextshell%==bash ( + CALL :getWslPath %winpath% wslpath + REM ECHO wslfullpath "!wslpath!%fname%" + CALL %nextshell% "!wslpath!%fname%" %* & SET task_exitcode=!errorlevel! + ) ELSE ( + REM probably tclsh or sh + IF NOT "x%keyRemoved%"=="x%validshells%" ( + REM sh uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl + REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx + CALL %nextshell% "%~dp0%fname%" %* & SET task_exitcode=!errorlevel! + ) ELSE ( + ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells% + SET task_exitcode=66 + GOTO :exit + ) + ) +) +@GOTO :endlib +:getWslPath +@SETLOCAL + @SET "_path=%~p1" + @SET "name=%~nx1" + @SET "drive=%~d1" + @SET "rtrn=%~2" + @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" +@ENDLOCAL & ( + @if "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + ECHO %result% + ) +) +@GOTO :eof +:endlib + +: \ +@REM @SET taskexit_code=!errorlevel! & goto :exit +@GOTO :exit +# } +# rem call %nextshell% "%~dp0%~n0.cmd" %* +# -*- tcl -*- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- tcl script section +# -- This is a punk multishell file +# -- Primary payload target is Tcl, with sh,bash,powershell as helpers +# -- but it may equally be used with any of these being the primary script. +# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script +# -- i.e it is a polyglot file. +# -- The specific layout including some lines that appear just as comments is quite sensitive to change. +# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline. +# -- e.g ./filename.polypunk.cmd in sh or bash +# -- e.g tclsh filename.cmd +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +rename set ""; rename s2 set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup +Hide :exit;Hide {<#};Hide '@ +namespace eval ::punk::multishell { + set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script [file dirname [file normalize [info script]/__]] + if {[info exists argv0] && + $last_script eq $last_script_root + } { + set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode + } else { + set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. + } + if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { + proc ::punk::multishell::is_main {{script_name {}}} { + if {$script_name eq ""} { + set script_name [file dirname [file normalize [info script]/--]] + } + if {![info exists ::punk::multishell::is_main($script_name)]} { + #e.g a .dll or something else unanticipated + puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting" + puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]" + return 0 + } + return [set ::punk::multishell::is_main($script_name)] + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" +#puts "argv0 : $::argv0" +# -- --- --- --- --- --- --- --- --- --- --- --- + + +# +puts stdout "launching punk87" + +set dirname [file dirname [info script]] +if {[file tail $dirname] eq "bin"} { + if {[file exists [file join $dirname ../src/punk86.vfs/main.tcl]]} { + #tclsh [file join $dirname ../src/punk86.vfs/main.tcl] {*}$::argv + source [file join $dirname ../src/punk86.vfs/main.tcl] + } else { + puts stderr "Unable to locate punk87 entry-point main.tcl" + } +} else { + puts stderr "punk87 launch script must be run from the punk bin folder" +} +puts stdout "-done-" + + +# + + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- Best practice is to always return or exit above, or just by leaving the below defaults in place. +# -- If the multishell script is modified to have Tcl below the Tcl Payload section, +# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. +# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below +# -- but the sh/bash 'then' and 'fi' would also need to be uncommented. +# -- This facility left in place for experiments on whether configuration payloads etc can be appended +# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells +# -- can be made to ignore/cope with such data. +if {[::punk::multishell::is_main]} { + exit 0 +} else { + return +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +# end hide from unix shells \ +HEREDOC1B_HIDE_FROM_BASH_AND_SH +# sh/bash \ +shift && set -- "${@:1:$#-1}" +#------------------------------------------------------ +# -- This if block only needed if Tcl didn't exit or return above. +if false==false # else { + then + : +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- sh/bash script section +# -- leave as is if all that is required is launching the Tcl payload" +# -- +# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default +# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate +# -- if sh/bash scripting needs to run on windows too. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +#printf "start of bash or sh code" + +# +# + +# -- --- --- --- --- --- --- --- +# +exitcode=0 ;#default assumption +#-- sh/bash launches Tcl here instead of shebang line at top +#-- use exec to use exitcode (if any) directly from the tcl script +#exec /usr/bin/env tclsh "$0" "$@" +#-- alternative - can run sh/bash script after the tcl call. +/usr/bin/env tclsh "$0" "$@" +exitcode=$? +#echo "tcl exitcode: ${exitcode}" +#-- override exitcode example +#exit 66 +# +# -- --- --- --- --- --- --- --- + +# +# + + +#printf "sh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload +#------------------------------------------------------ +fi +exit ${exitcode} +# end hide sh/bash block from Tcl +# This comment with closing brace should stay in place whether if commented or not } +#------------------------------------------------------ +# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above +if 0 { +: end heredoc1 - end hide from powershell \ +'@ +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- powershell/pwsh section +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +function GetScriptName { $myInvocation.ScriptName } +$scriptname = getScriptName +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +#"Script Name : {0}" -f $scriptname | write-host +#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +#"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- +# +tclsh $scriptname $args +# +# -- --- --- --- --- --- --- --- + + +# +# + +# unbal } + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host +Exit $LASTEXITCODE +# heredoc2 for powershell to ignore block below +$1 = @' +' +: end hide powershell-block from Tcl \ +# This comment with closing brace should stay in place whether 'if' commented or not } +: cmd exit label - return exitcode +:exit +: \ +@REM @ECHO exitcode: !task_exitcode! +: \ +@EXIT /B !task_exitcode! +# cmd has exited +: end heredoc2 \ +'@ +<# +# id:tailblock0 +# -- powershell multiline comment +#> +<# +# id:tailblock1 +# + +# +# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A) +# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data +# -- so for example a plain text tar archive could cause problems depending on the content. +# -- final line in file must be the powershell multiline comment terminator or other data it can handle. +# -- e.g plain # comment lines will work too +# -- (for example a powershell digital signature is a # commented block of data at the end of the file) +#> + + diff --git a/src/scriptapps/punk87.tcl b/src/scriptapps/punk87.tcl new file mode 100644 index 00000000..09eb06f6 --- /dev/null +++ b/src/scriptapps/punk87.tcl @@ -0,0 +1,15 @@ +puts stdout "launching punk87" + +set dirname [file dirname [info script]] +if {[file tail $dirname] eq "bin"} { + if {[file exists [file join $dirname ../src/punk86.vfs/main.tcl]]} { + #tclsh [file join $dirname ../src/punk86.vfs/main.tcl] {*}$::argv + source [file join $dirname ../src/punk86.vfs/main.tcl] + } else { + puts stderr "Unable to locate punk87 entry-point main.tcl" + } +} else { + puts stderr "punk87 launch script must be run from the punk bin folder" +} +puts stdout "-done-" + diff --git a/src/vendormodules/overtype-1.5.0.tm b/src/vendormodules/overtype-1.5.0.tm index b1d37619..f4e466f3 100644 --- a/src/vendormodules/overtype-1.5.0.tm +++ b/src/vendormodules/overtype-1.5.0.tm @@ -82,9 +82,16 @@ namespace eval overtype { # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\ } + +#candidate for zig/c implementation? proc overtype::stripansi {text} { variable escape_terminals ;#dict variable ansi_2byte_codes_dict + #important that we don't spend too much time on this for plain text that doesn't contain any escapes anyway + if {[string first \033 $text] <0 && [string first \009c $text] <0} { + #\033 same as \x1b + return $text + } set text [convert_g0 $text] @@ -167,6 +174,7 @@ proc overtype::stripansi_gx {text} { } +#This shouldn't be called on text containing ansi codes! proc overtype::strip_nonprinting_ascii {str} { #review - some single-byte 'control' chars have visual representations e.g ETX as heart #It is currently used for screen display width calculations @@ -230,7 +238,7 @@ proc overtype::printing_length {line} { } } set line2 [join $outchars ""] - return [string_columns $line2] + return [punk::char::string_width $line2] } proc overtype::string_columns {text} { @@ -262,7 +270,7 @@ proc overtype::left {args} { if {[llength $args] < 2} { error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} } - foreach {underblock overblock} [lrange $args end-1 end] break + lassign [lrange $args end-1 end] underblock overblock set defaults [dict create\ -ellipsis 0\ -ellipsistext $default_ellipsis_horizontal\ @@ -322,7 +330,7 @@ proc overtype::left {args} { } -namespace eval |> { +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} @@ -546,7 +554,7 @@ proc overtype::renderline {args} { if {[llength $args] < 2} { error {usage: ?-start ? ?-transparent [0|1|]? ?-overflow [1|0]? undertext overtext} } - foreach {under over} [lrange $args end-1 end] break + lassign [lrange $args end-1 end] under over if {[string first \n $under] >=0 || [string first \n $over] >= 0} { error "overtype::renderline not allowed to contain newlines" } @@ -588,13 +596,18 @@ proc overtype::renderline {args} { set opt_exposed2 [dict get $opts -exposed2] # -- --- --- --- --- --- --- --- --- --- --- --- + #----- + # if {[string first \t $under] >= 0} { - set under [textutil::tabify::untabify2 $under] + #set under [textutil::tabify::untabify2 $under] + set under [textutil::tabify::untabifyLine $under 8] ;#8 is default for untabify2 - review } set overdata $over if {[string first \t $over] >= 0} { - set overdata [textutil::tabify::untabify2 $over] + #set overdata [textutil::tabify::untabify2 $over] + set overdata [textutil::tabify::untabifyLine $over 8] } + #------- #ta_detect ansi and do simpler processing? @@ -613,7 +626,7 @@ proc overtype::renderline {args} { #pt = plain text append pt_underchars $pt foreach ch [split $pt ""] { - set width [string_columns $ch] + set width [punk::char::string_width $ch] incr i_u dict set understacks $i_u $u_codestack lappend out $ch @@ -695,13 +708,13 @@ proc overtype::renderline {args} { incr idx } elseif {($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) - set owidth [string_columns $ch] + set owidth [punk::char::string_width $ch] if {$idx > [llength $out]-1} { lappend out " " dict set understacks $idx [list] ;#review - use idx-1 codestack? incr idx } else { - set uwidth [string_columns [lindex $out $idx]] + set uwidth [punk::char::string_width [lindex $out $idx]] if {[lindex $out $idx] eq ""} { #2nd col of 2-wide char in underlay incr idx @@ -714,7 +727,7 @@ proc overtype::renderline {args} { incr idx } } elseif {$uwidth > 1} { - if {[string_columns $ch] == 1} { + if {[punk::char::string_width $ch] == 1} { #normal singlewide transparency set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay if {$next_pt_overchar eq ""} { @@ -737,8 +750,8 @@ proc overtype::renderline {args} { } } else { #non-transparent char in overlay - set owidth [string_columns $ch] - set uwidth [string_columns [lindex $out $idx]] + set owidth [punk::char::string_width $ch] + set uwidth [punk::char::string_width [lindex $out $idx]] if {[lindex $out $idx] eq ""} { #2nd col of 2wide char in underlay priv::render_addchar $idx $ch [dict get $overstacks $idx_over] @@ -782,8 +795,8 @@ proc overtype::renderline {args} { if {$opt_overflow == 0} { #need to truncate to the width of the original undertext - #review - string_columns vs printing_length here. undertext requirement to be already rendered therefore string_columns ok? - set num_under_columns [string_columns $pt_underchars] ;#plaintext underchars + #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 } #coalesce and replay codestacks for out char list @@ -798,7 +811,7 @@ proc overtype::renderline {args} { foreach ch $out { append out_rawchars $ch if {$opt_overflow == 0 && !$in_overflow} { - if {[set nextvisualwidth [string_columns $out_rawchars]] < $num_under_columns} { + if {[set nextvisualwidth [punk::char::string_width $out_rawchars]] < $num_under_columns} { } else { #todo - check if we overflowed with a double-width char ? #store visualwidth which may be short