Browse Source

+punk::winrun, punk::ns, punk pipeline fixes

master
Julian Noble 1 year ago
parent
commit
39160bf742
  1. 11
      scriptlib/showargs.tcl
  2. 28
      scriptlib/showargsplus.tcl
  3. 285
      src/modules/calc676-999999.0a1.0.tm
  4. 3
      src/modules/calc676-buildversion.txt
  5. 225
      src/modules/punk-0.1.tm
  6. 4
      src/modules/punk/cap-999999.0a1.0.tm
  7. 2
      src/modules/punk/char-999999.0a1.0.tm
  8. 30
      src/modules/punk/console-999999.0a1.0.tm
  9. 12
      src/modules/punk/mix/commandset/layout-999999.0a1.0.tm
  10. 6
      src/modules/punk/mix/commandset/module-999999.0a1.0.tm
  11. 10
      src/modules/punk/mix/commandset/project-999999.0a1.0.tm
  12. 34
      src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm
  13. 537
      src/modules/punk/ns-999999.0a1.0.tm
  14. 51
      src/modules/punk/tcl-999999.0a1.0.tm
  15. 3
      src/modules/punk/tcl-buildversion.txt
  16. 8
      src/modules/punk/unixywindows-999999.0a1.0.tm
  17. 817
      src/modules/punk/winrun-999999.0a1.0.tm
  18. 3
      src/modules/punk/winrun-buildversion.txt
  19. 13
      src/modules/shellfilter-0.1.8.tm
  20. 85
      src/modules/shellrun-0.1.tm
  21. 286
      src/scriptapps/punk87.cmd
  22. 15
      src/scriptapps/punk87.tcl
  23. 43
      src/vendormodules/overtype-1.5.0.tm

11
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]
}

28
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

285
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 <pkg>-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 <unspecified>
# @@ 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

3
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.

225
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 " " "<sp>" \t "<tab>"] $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 <value> $datasource] {
set insertion_data <value>
set insertion_data "<value>" ;#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} {
} 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,35 +3442,30 @@ 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]} {
#puts "======> recurse assign 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]
}
}
}
set procname $initial_returnvarspec.=$equalsrhs
@ -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=]*)\=([^\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=]*)\=([^ \t\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/

4
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"
}
}
}

2
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 ""

30
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]"

12
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 {

6
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]
}

10
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
}

34
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 {

537
src/modules/punk/ns-999999.0a1.0.tm

@ -26,76 +26,79 @@ 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]]
} else {
set out [nslist [nsjoin $ns_current *] -types [list all]]
}
#todo - cooperate with repl
set ns_display "\n$ns_current"
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 types [list all]
set nspathcommands 0
if {$v eq "/"} {
set types [list children]
}
if {$v eq "///"} {
set nspathcommands 1
}
append out $ns_display
return $out
#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 atail [lassign $args a1]
if {$a1 in [list :: ""]} {
set ns_current ::
tailcall ns/ $v {*}$atail
}
set is_absolute [string match ::* $a1]
set is_absolute [string match ::* $ns_or_glob]
set has_globchars [regexp {[*?]} $ns_or_glob]
if {$is_absolute} {
if {![llength $atail] && [regexp {[*?]} $a1]} {
#set out [get_nslist -match $a1]
set out [nslist $a1]
append out "\n$a1"
return $out
if {!$has_globchars} {
if {![namespace exists $ns_or_glob]} {
error "cannot change to namespace $ns_or_glob"
}
set nsparent [nsprefix $a1]
set nstail [nstail $a1]
if {[nseval $nsparent [list ::namespace exists $nstail]]} {
set ns_current $a1
tailcall ns/ $v {*}$atail
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]
}
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 {!$has_globchars} {
set nsnext [nsjoin $ns_current $ns_or_glob]
if {![namespace exists $nsnext]} {
error "cannot change to namespace $ns_or_glob"
}
if {[nseval $ns_current [list ::namespace exists $a1]]} {
set ns_current $nsnext
tailcall ns/ $v {*}$atail
set ns_queried $nsnext
set out [nslist [nsjoin $nsnext *] -types $types -nspathcommands $nspathcommands]
} else {
error "cannot change to namespace $nsnext"
set ns_queried [nsjoin $ns_current $ns_or_glob]
set out [nslist [nsjoin $ns_current $ns_or_glob] -types $types -nspathcommands $nspathcommands]
}
}
}
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 (ensemble)[a+]"
}
}
}
}
append out $ns_display
return $out
}
#create possibly nested namespace structure - but only if not already existant
@ -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,13 +424,20 @@ 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
if {$has_globchars} {
foreach seg $parts {
if {![regexp {[*?]} $seg]} {
set base [nsjoin $base $seg]
@ -424,12 +447,15 @@ namespace eval punk::ns {
}
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,12 +480,18 @@ 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]]
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 {
#puts "nstree_list: no tailparts base:$base"
@ -514,7 +545,7 @@ namespace eval punk::ns {
#puts stderr "> adding $base to $nslist"
set nslist [list $base {*}$nslist]
}
if {$has_globchars} {
if {$allbelow} {
foreach ns $nslist {
if {[globmatchns ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} {
@ -524,6 +555,18 @@ namespace eval punk::ns {
} else {
set nslist_filtered [struct::list::Lfilter $nslist [list globmatchns ${ns_absolute}]]
}
} else {
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
}
return $nslist
@ -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]
}
if {[string length $block]} {
append output \n $block
}
append output $block
if {$count_with_results > 1 } {
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 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 commands [list]
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,32 +1203,19 @@ namespace eval punk::ns {
::continue
}
#NOTE - matched commands will return commands from global ns due to 'namespace eval'
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 {
#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}]
::foreach c $cmd_tails {
::if {$c in $all_ns_tails} {
::if {$do_raw} {
@ -1146,12 +1225,96 @@ namespace eval punk::ns {
}
}
}
} else {
::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]
}
}
}
}
::list ok [::list result $commandlist]
#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.
} |data@@ok/result> ::lsort -index 0 |> {::lmap v $data {::lindex $v 1}} |> {::if {![::llength $data]} {::return {}} else {::return [::join $data \n]\n}} <searchlist|
proc nscommands {args} {
set commandns [uplevel 1 [list ::namespace current]]
set commandlist [::list]
#color code multiple args? - not very practical if many ns args supplied, but then... common case will only be 1 or 2 anyway
#colors can be stripped by caller with ansistrip - but we'll make the default case easier by using no ansi codes if a single arg is passed
set colors [::list none cyan yellow green]
set ci 0 ;#colourindex
set do_raw 0
if {[::set posn [::lsearch $args -raw]] >= 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> $v] {
trace add variable <v> 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

51
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 <pkg>-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 <unspecified>
# @@ 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

3
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.

8
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

817
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 <pkg>-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 <func> $e <a> $source_ns] {
set cmd ""
if {![catch {namespace import <a>::<func>}]} {
set cmd <func>
}
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

3
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.

13
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

85
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

286
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"
# -- --- --- --- --- --- --- --- --- --- --- ---
#<tcl-payload>
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-"
#</tcl-payload>
# -- --- --- --- --- --- --- --- --- --- --- ---
# -- 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"
#<shell-payload-pre-tcl>
#</shell-payload-pre-tcl>
# -- --- --- --- --- --- --- ---
#<shell-launch-tcl>
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
#</shell-launch-tcl>
# -- --- --- --- --- --- --- ---
#<shell-payload-post-tcl>
#</shell-payload-post-tcl>
#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
# -- --- --- ---
#<powershell-payload-pre-tcl>
#</powershell-payload-pre-tcl>
# -- --- --- --- --- --- --- ---
#<powershell-launch-tcl>
tclsh $scriptname $args
#</powershell-launch-tcl>
# -- --- --- --- --- --- --- ---
#<powershell-payload-post-tcl>
#</powershell-payload-post-tcl>
# 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
# <ctrl-z>

# </ctrl-z>
# -- unreachable by tcl directly if ctrl-z character is in the <ctrl-z> 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)
#>

15
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-"

43
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|<regexp>]? ?-exposed1 <char>? ?-exposed2 <char>? ?-overflow [1|0]? overtext pipelinedata}
@ -546,7 +554,7 @@ proc overtype::renderline {args} {
if {[llength $args] < 2} {
error {usage: ?-start <int>? ?-transparent [0|1|<regexp>]? ?-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

Loading…
Cancel
Save