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 -nonewline stdout "info script\r\n"
#puts stdout "[info script]" #puts stdout "[info script]"
puts stdout "::argc" puts stdout "argc: $::argc"
puts stdout $::argc puts stdout "argv one arg per line, each line followed by dotted line."
puts stdout "::argv" foreach a $::argv {
puts stdout "$::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:@.(),]+?)(?![^(]*\))(,.*)*$} #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. #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 #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 scan $s %${p}s%s
} }
proc _split_patterns {varspecs} { 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]} { if {$cmdname in [info commands $cmdname]} {
return [$cmdname] return [$cmdname]
} }
set varlist [list] 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 ^ #except when prefixed directly by pin classifier ^
set protect_terminals [list "^"] ;# e.g sequence ^# set protect_terminals [list "^"] ;# e.g sequence ^#
#also - an atom usually doesn't need the / as a terminal - because it can't match a missing element unless it's empty string #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] 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 #same as used in unknown func for initial launch
#variable re_assign {^([^\r\n=\{]*)=(.*)} #variable re_assign {^([^\r\n=\{]*)=(.*)}
#variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)} #variable re_assign {^[\{]{0,1}([^ \t\r\n=]*)=(.*)}
@ -2872,12 +2866,16 @@ namespace eval punk {
#puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args" #puts stderr ">> match_assign '$scopepattern=$equalsrhs' $args"
set fulltail $args set fulltail $args
set homens ::punk::pipecmds set homens ::punk::pipecmds
set rhsmapping [pipecmd_rhsmapping $equalsrhs]
set pipecmd ${homens}::$scopepattern=$equalsrhs set pipecmd ${homens}::$scopepattern=$rhsmapping
#pipecmd could have glob chars - test $pipcmd in the list - not just that info commands returns results. #pipecmd could have glob chars - test $pipcmd in the list - not just that info commands returns results.
if {$pipecmd in [info commands $pipecmd]} { if {$pipecmd in [info commands $pipecmd]} {
#puts "==nscaller: '[uplevel 1 [list namespace current]]'" #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 tailcall $pipecmd {*}$args
} }
@ -2990,7 +2988,7 @@ namespace eval punk {
set datasource $v set datasource $v
} }
append script [string map [list <value> $datasource] { append script [string map [list <value> $datasource] {
set insertion_data <value> set insertion_data "<value>" ;#atom could have whitespace
}] }]
set needs_insertion 1 set needs_insertion 1
@ -3038,7 +3036,10 @@ namespace eval punk {
debug.punk.pipe.compile {match_assign creating proc $pipecmd} 2 debug.punk.pipe.compile {match_assign creating proc $pipecmd} 2
uplevel 1 [list ::proc $pipecmd args $script] 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 tailcall $pipecmd {*}$args
} }
@ -3344,19 +3345,54 @@ namespace eval punk {
} }
} }
#exclude quoted whitespace
proc arg_is_script_shaped {arg} { proc arg_is_script_shaped {arg} {
if {[string first " " $arg] >= 0} { if {[string first \n $arg] >= 0} {
return 1
} elseif {[string first \n $arg] >= 0} {
return 1 return 1
} elseif {[string first ";" $arg] >= 0} { } elseif {[string first ";" $arg] >= 0} {
return 1 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 return 1
}
} else { } else {
return 0 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} { proc pipeline {segment_op initial_returnvarspec equalsrhs args} {
set fulltail $args set fulltail $args
@ -3406,35 +3442,30 @@ namespace eval punk {
#handle for example: #handle for example:
#var1.= var2= "etc" |> string toupper #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. #*SUB* pipeline recursion.
#puts "======> recurse based on next1:$next1 " #puts "======> recurse based on next1:$next1 "
#set re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} if {[string index $next1 $nexteposn-1] eq {.}} {
if {[regexp {^([^ \t\r\n=\{]*)\.=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #var1.= var2.= ...
#non pipelined call to self - return result #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]] set results [uplevel 1 [list $next1 {*}$nexttail]]
#debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5 #debug.punk.pipe.rep {==> rep recursive results: [rep $results]} 5
#debug.punk.pipe {>>> results: $results} 1 #debug.punk.pipe {>>> results: $results} 1
return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]] return [_handle_bind_result [_multi_bind_result $initial_returnvarspec $results]]
} }
#puts "======> recurse assign based on next1:$next1 "
#puts "======> recurse asssign based on next1:$next1 " #if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} {
if {[regexp {^([^ \t\r\n=\{]*)=(.*)} $next1 _ nextreturnvarspec nextrhs]} { #}
#non pipelined call to plain = assignment - return result #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]] set results [uplevel 1 [list $next1 {*}$nexttail]]
#debug.punk.pipe {>>> results: $results} 1 #debug.punk.pipe {>>> results: $results} 1
set d [_multi_bind_result $initial_returnvarspec $results] set d [_multi_bind_result $initial_returnvarspec $results]
return [_handle_bind_result $d] return [_handle_bind_result $d]
} }
} }
}
set procname $initial_returnvarspec.=$equalsrhs 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 @* 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}] 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.var {segment_has_insertions: $insertion_patterns} 5
debug.punk.pipe.rep {[rep_listname segment_members]} 4 debug.punk.pipe.rep {[rep_listname segment_members]} 4
@ -3650,8 +3683,8 @@ namespace eval punk {
set segment_members_filled [list] set segment_members_filled [list]
set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign set segmenttail $segment_members ;# todo - change to segment_members here to match punk::match_assign
set rhsmapped [pipecmd_rhsmapping $rhs]
set cmdname "::punk::pipecmds::insertion_$rhs" set cmdname "::punk::pipecmds::insertion_$rhsmapped"
#commandname can contain glob chars - must search for exact membership in 'info commands' result. #commandname can contain glob chars - must search for exact membership in 'info commands' result.
if {$cmdname ni [info commands $cmdname]} { if {$cmdname ni [info commands $cmdname]} {
@ -3671,7 +3704,7 @@ namespace eval punk {
if {[string length $indexspec]} { if {[string length $indexspec]} {
error "pipesyntax - index not supported on atom" "pipeline $segment_op $initial_returnvarspec $equalsrhs $args" [list pipesyntax index_on_literal] 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]} { } elseif {[string is double -strict $v]} {
#don't treat numbers as variables #don't treat numbers as variables
if {[string length $indexspec]} { if {[string length $indexspec]} {
@ -3720,17 +3753,17 @@ namespace eval punk {
append insertion_script \n {set segmenttail} append insertion_script \n {set segmenttail}
append insertion_script \n "}" append insertion_script \n "}"
#puts stderr "$insertion_script" #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 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 #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) #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 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 #regexp $punk::re_assign $hd _ pattern equalsrhs
#we assume the whole pipeline has been provided as the head #we assume the whole pipeline has been provided as the head
regexp {^([^\t\r\n=]*)\=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs tail #regexp {^([^\t\r\n=]*)\=([^ \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 #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 # 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)" error "unknown_assign_dispatch: namespace '$ns' not found. (Note that pipeline lhs variables cannot be namespaced)"
} else { } else {
set nscaller [uplevel 1 [list ::namespace current]] 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. #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'" puts stderr "unknown_assign_dispatch>> '$pattern=$equalsrhs' $commands nscaller: '$nscaller'"
#we call the namespaced function - we don't evaluate it *in* the namespace. #we call the namespaced function - we don't evaluate it *in* the namespace.
#REVIEW #REVIEW
#warn for now...? #warn for now...?
#tailcall $pattern=$equalsrhs {*}$args #tailcall $pattern=$equalsrhs {*}$args
tailcall $pattern=$equalsrhs {*}$tail tailcall $pattern=$rhsmapped {*}$tail
} }
} }
#puts "--->nscurrent [uplevel 1 [list ::namespace current]]" #puts "--->nscurrent [uplevel 1 [list ::namespace current]]"
@ -4216,19 +4253,11 @@ namespace eval punk {
#e.g x=a\nb c #e.g x=a\nb c
#x will be assigned the list {a\nb c} ie the spaces between b & c are not maintained #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} { proc ::punk::_unknown_compare {val1 val2 args} {
@ -4272,6 +4301,9 @@ namespace eval punk {
# #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]] # #return [uplevel 1 [list ::punk::match_exec $varspecs $rhs {*}$tail]]
# } # }
# #
proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} { proc ::punk::_unknown_dot_assign_dispatch {partzerozero pattern equalsrhs args} {
set argstail [lassign $args hd] set argstail [lassign $args hd]
@ -4286,17 +4318,20 @@ namespace eval punk {
#regexp $punk::re_assign $hd _ pattern equalsrhs #regexp $punk::re_assign $hd _ pattern equalsrhs
#we assume the whole pipeline has been provided as the head #we assume the whole pipeline has been provided as the head
#regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail #regexp {^([^ \t\r\n=\{]*)\.=([^ \t\r\n]*)(.*)} $hd _ pattern equalsrhs argstail
regexp {^([^ \t\r\n=\{]*)\.=([^ \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 #tailcall ::punk::match_assign $pattern $equalsrhs {*}$argstail
return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]] return [uplevel 1 [list ::punk::pipeline .= $pattern $equalsrhs {*}$argstail]]
} }
#variable re_dot_assign {^([^ \t\r\n=\{]*)\.=(.*)} #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]} { #know {[regexp $punk::re_dot_assign [lindex $args 0 0] partzerozero varspecs rhs]} {
# set argstail [lassign $args hd] # set argstail [lassign $args hd]
@ -4323,18 +4358,7 @@ namespace eval punk {
tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist] tailcall {*}[list ::punk::pipeline = "" "" {*}$arglist]
} }
#maint - punk::arg_is_script_shaped (inlined) set is_script [punk::arg_is_script_shaped $assign]
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
}
if {!$is_script && [string index $assign end] eq "="} { if {!$is_script && [string index $assign end] eq "="} {
#set re_dotequals {^([^ \t\r\n=\{]*)\.=$} #set re_dotequals {^([^ \t\r\n=\{]*)\.=$}
@ -5069,6 +5093,7 @@ namespace eval punk {
#todo - in thread #todo - in thread
#todo - streaming version #todo - streaming version
proc dirfiles_dict {{searchspec ""}} { proc dirfiles_dict {{searchspec ""}} {
package require vfs
#we don't want to normalize.. #we don't want to normalize..
#for example if the user supplies ../ we want to see ../result #for example if the user supplies ../ we want to see ../result
if {[file pathtype $searchspec] eq "relative"} { if {[file pathtype $searchspec] eq "relative"} {
@ -5340,6 +5365,18 @@ namespace eval punk {
interp alias {} ./new {} punk::d/new interp alias {} ./new {} punk::d/new
interp alias {} d/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. #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 #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} { proc help_chunks {args} {
set chunks [list] set chunks [list]
set linesep [string repeat - 76] set linesep [string repeat - 76]
set mascotblock " "
catch { catch {
package require patternpunk 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 text ""
set known $::punk::config::known_punk_env_vars set known $::punk::config::known_punk_env_vars
append text $linesep\n append text $linesep\n
@ -6393,7 +6433,7 @@ namespace eval punk {
lappend chunks [list stdout $text] lappend chunks [list stdout $text]
set text "" set text ""
append text "Punk commands:\n" append text "Punk core navigation commands:\n"
append text " help\n" append text " help\n"
#todo - load from source code annotation? #todo - load from source code annotation?
@ -6402,10 +6442,10 @@ namespace eval punk {
lappend cmdinfo [list ./ "view/change directory"] lappend cmdinfo [list ./ "view/change directory"]
lappend cmdinfo [list ../ "go up one directory"] lappend cmdinfo [list ../ "go up one directory"]
lappend cmdinfo [list ./new "make new directory and switch to it"] 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 n/ "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 n// "view/change namespace (with command listing)"]
lappend cmdinfo [list ::/ "go up one namespace"] lappend cmdinfo [list nn/ "go up one namespace"]
lappend cmdinfo [list :/new "make child namespace and switch to it"] lappend cmdinfo [list n/new "make child namespace and switch to it"]
set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *] set cmds [lsearch -all -inline -index 0 -subindices $cmdinfo *]
set descr [lsearch -all -inline -index 1 -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 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]} { if {[punkrepl::has_script_var_bug]} {
append text "Has script var bug! (string rep for list variable in script generated when script changed)" 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 . & .. 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? # -v for natural number sorting not supported on freeBSD. Todo - test at startup and modify aliases?
#interp alias {} lw {} ls -aFv --color=always #interp alias {} lw {} ls -aFv --color=always
interp alias {} ./ {} punk::d/ interp alias {} ./ {} punk::d/
interp alias {} ../ {} punk::dd/ interp alias {} ../ {} punk::dd/
interp alias {} d/ {} punk::d/ 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]} { if {[file isdirectory $tpath]} {
dict set folderdict $tpath [list source $pkg sourcetype package] dict set folderdict $tpath [list source $pkg sourcetype package]
} else { } 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 { } 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 search_this_and_that $args
set charcount 0 set charcount 0
set width_results [dict create]
puts stdout "calibrating using terminal cursor movements.." puts stdout "calibrating using terminal cursor movements.."
foreach charsetname $matched_names { foreach charsetname $matched_names {
if {[llength $search_this_and_that]} { if {[llength $search_this_and_that]} {
@ -1692,7 +1693,6 @@ namespace eval punk::char {
if {![dict size $charset_dict]} { if {![dict size $charset_dict]} {
continue continue
} }
set width_results [dict create]
dict for {hex inf} $charset_dict { dict for {hex inf} $charset_dict {
set ch [format %c 0x$hex] set ch [format %c 0x$hex]
set twidth "" 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 { namespace eval internal {
proc abort_if_loop {{failmsg ""}} { proc abort_if_loop {{failmsg ""}} {
#puts "il1 [info level 1]" #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 template_folder_dict [punk::mix::base::lib::get_template_folders]
set tpldirs [list] set tpldirs [list]
dict for {dir folderinfo} $template_folder_dict { dict for {tdir folderinfo} $template_folder_dict {
if {[file exists $dir/layouts/$layout]} { if {[file exists $tdir/layouts/$layout]} {
lappend tpldirs $dir lappend tpldirs $tdir
} }
} }
if {![llength $tpldirs]} { if {![llength $tpldirs]} {
@ -75,8 +75,8 @@ namespace eval punk::mix::commandset::layout {
set layouts [list] set layouts [list]
#set tplfolderdict [punk::cap::templates::folders] #set tplfolderdict [punk::cap::templates::folders]
set tplfolderdict [punk::mix::base::lib::get_template_folders] set tplfolderdict [punk::mix::base::lib::get_template_folders]
dict for {tpldir folderinfo} $tplfolderdict { dict for {tdir folderinfo} $tplfolderdict {
set layout_base $tpldir/layouts set layout_base $tdir/layouts
#collect all layouts and use lsearch glob rather than the filesystem glob (avoid issues with dotted folder names) #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 *]] set all_layouts [lsort [glob -nocomplain -dir $layout_base -type d -tail *]]
foreach match [lsearch -all -inline $all_layouts $glob] { foreach match [lsearch -all -inline $all_layouts $glob] {
@ -107,7 +107,7 @@ namespace eval punk::mix::commandset::layout {
set layoutfolder [lindex $layouts_found end] set layoutfolder [lindex $layouts_found end]
if {![file isdirectory $layoutfolder]} { 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] set file_list [list]
util::foreach-file $layoutfolder path { 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 opt_scriptpath [dict get $opts -scriptpath]
set module_tfolders [list] set module_tfolders [list]
set tfolders [punk::mix::base::lib::get_template_folders $opt_scriptpath] set tfolderdict [punk::mix::base::lib::get_template_folders $opt_scriptpath]
foreach tf $tfolders { dict for {tdir folderinfo} $tfolderdict {
lappend module_tfolders [file join $tf module] 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 template_folder_dict [punk::mix::base::lib::get_template_folders]
set tpldirs [list] set tpldirs [list]
dict for {dir folderinfo} $template_folder_dict { dict for {tdir folderinfo} $template_folder_dict {
if {[file exists $dir/layouts/$opt_layout]} { if {[file exists $tdir/layouts/$opt_layout]} {
lappend tpldirs $dir lappend tpldirs $tdir
} }
} }
if {![llength $tpldirs]} { if {![llength $tpldirs]} {
puts stderr "layout '$opt_layout' was not found in template dirs" puts stderr "layout '$opt_layout' was not found in template dirs"
puts stderr "searched [dict size $template_folder_dict] template folders" puts stderr "searched [dict size $template_folder_dict] template folders"
dict for {dir folderinfo} $template_folder_dict { dict for {tdir folderinfo} $template_folder_dict {
puts stderr " - $dir $folderinfo" puts stderr " - $tdir $folderinfo"
} }
return 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 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] set tpldirs [list]
dict for {dir pkg} $template_folder_dict { dict for {tdir tsourceinfo} $template_folder_dict {
if {[file exists $dir/utility/scriptappwrappers/$templatename]} { if {[file exists $tdir/utility/scriptappwrappers/$templatename]} {
lappend tpldirs $dir lappend tpldirs $tdir
} }
} }
@ -261,12 +263,20 @@ namespace eval punk::mix::commandset::scriptwrap {
append msg \n "Searched [dict size $template_folder_dict] template dirs" append msg \n "Searched [dict size $template_folder_dict] template dirs"
error $msg 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]} { if {$wrapper_template eq "" || ![file exists $wrapper_template]} {
error "wrap_in_multishell: unable to find multishell template at $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] set tpldirs [list]
dict for {dir pkg} $template_folder_dict { dict for {tdir tsourceinfo} $template_folder_dict {
if {[file exists $dir/utility/scriptappwrappers]} { if {[file exists $tdir/utility/scriptappwrappers]} {
lappend tpldirs $dir lappend tpldirs $tdir
} }
} }
foreach tpldir $tpldirs { 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 { namespace eval punk::ns {
variable ns_current "::" 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 namespace export nsjoin nsprefix nstail nsparts nseval nsimport_noclobber corp
#leading colon makes it hard (impossible?) to call directly if not within the namespace #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 {ns_or_glob ""} args} {
proc ns/ {v args} {
variable ns_current ;#change active ns of repl by setting ns_current variable ns_current ;#change active ns of repl by setting ns_current
set ns_caller [uplevel 1 {::namespace current}] set ns_caller [uplevel 1 {::namespace current}]
#puts stderr "ns_cur:$ns_current ns_call:$ns_caller" #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" set types [list all]
if {$ns_current in [info commands $ns_current] } { set nspathcommands 0
if {![catch [list namespace ensemble configure $ns_current] ensemble_info]} { if {$v eq "/"} {
if {[llength $ensemble_info] > 0} { set types [list children]
#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+]"
}
} }
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 { } else {
set atail [lassign $args a1] set is_absolute [string match ::* $ns_or_glob]
if {$a1 in [list :: ""]} { set has_globchars [regexp {[*?]} $ns_or_glob]
set ns_current ::
tailcall ns/ $v {*}$atail
}
set is_absolute [string match ::* $a1]
if {$is_absolute} { if {$is_absolute} {
if {![llength $atail] && [regexp {[*?]} $a1]} { if {!$has_globchars} {
#set out [get_nslist -match $a1] if {![namespace exists $ns_or_glob]} {
set out [nslist $a1] error "cannot change to namespace $ns_or_glob"
append out "\n$a1"
return $out
} }
set nsparent [nsprefix $a1] set ns_current $ns_or_glob
set nstail [nstail $a1] set ns_queried $ns_current
if {[nseval $nsparent [list ::namespace exists $nstail]]} { tailcall ns/ $v ""
set ns_current $a1 } else {
tailcall ns/ $v {*}$atail set ns_queried $ns_or_glob
set out [nslist $ns_or_glob -types $types -nspathcommands $nspathcommands]
} }
error "cannot change to namespace $a1"
} else { } else {
set nsnext [nsjoin $ns_current $a1] if {!$has_globchars} {
if {![llength $atail] && [regexp {[*?]} $a1]} { set nsnext [nsjoin $ns_current $ns_or_glob]
#set out [get_nslist -match $nsnext] if {![namespace exists $nsnext]} {
set out [nslist $nsnext] error "cannot change to namespace $ns_or_glob"
append out "\n$nsnext"
return $out
} }
if {[nseval $ns_current [list ::namespace exists $a1]]} {
set ns_current $nsnext set ns_current $nsnext
tailcall ns/ $v {*}$atail set ns_queried $nsnext
set out [nslist [nsjoin $nsnext *] -types $types -nspathcommands $nspathcommands]
} else { } 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 #create possibly nested namespace structure - but only if not already existant
@ -381,7 +384,17 @@ namespace eval punk::ns {
return "^[join $pats ::]\$" return "^[join $pats ::]\$"
} }
proc globmatchns {glob path} { 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 ""}} { proc nstree {{location ""}} {
@ -392,6 +405,9 @@ namespace eval punk::ns {
list_as_lines [nstree_list $location] 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} { proc nstree_list {location args} {
package require struct::list package require struct::list
#puts "> nstree_list $location $args" #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 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 base ""
set tailparts "" set tailparts [list]
if {$CALLDEPTH == 0} { if {$CALLDEPTH == 0} {
set parts [nsparts $ns_absolute] set parts [nsparts $ns_absolute]
lset parts 0 :: lset parts 0 ::
set idx 0 set idx 0
if {$has_globchars} {
foreach seg $parts { foreach seg $parts {
if {![regexp {[*?]} $seg]} { if {![regexp {[*?]} $seg]} {
set base [nsjoin $base $seg] set base [nsjoin $base $seg]
@ -424,12 +447,15 @@ namespace eval punk::ns {
} }
incr idx incr idx
} }
} else {
set base $ns_absolute
}
} else { } else {
set base $location set base $location
set tailparts $subnslist set tailparts $subnslist
} }
if {![namespace exists $base]} { if {![namespace exists $base]} {
return "" return [list]
} }
#set parent [nsprefix $ns_absolute] #set parent [nsprefix $ns_absolute]
#set tail [nstail $ns_absolute] #set tail [nstail $ns_absolute]
@ -447,7 +473,6 @@ namespace eval punk::ns {
set nslist [nstree_list $base -subnslist {} -allbelow 1] set nslist [nstree_list $base -subnslist {} -allbelow 1]
} elseif {[regexp {[*]{2}$} $nextglob]} { } elseif {[regexp {[*]{2}$} $nextglob]} {
set nslist [list] set nslist [list]
#JMN
lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]]
foreach ch $nsmatches { foreach ch $nsmatches {
lappend nslist $ch 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] lappend nslist {*}[nstree_list $ch -subnslist [list "**" {*}[lrange $tailparts 1 end]] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow 0]
} }
} else { } 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] set nslist [list]
lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]] lappend nsmatches {*}[lsearch -all -inline -glob $allchildren [nsjoin ${base} $nextglob]]
if {[llength $tailparts] >1 || $allbelow} {
foreach ch $nsmatches { foreach ch $nsmatches {
lappend nslist $ch lappend nslist $ch
lappend nslist {*}[nstree_list $ch -subnslist [lrange $tailparts 1 end] -call-depth-internal [expr {$CALLDEPTH + 1}] -allbelow $allbelow] 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 { } else {
#puts "nstree_list: no tailparts base:$base" #puts "nstree_list: no tailparts base:$base"
@ -514,7 +545,7 @@ namespace eval punk::ns {
#puts stderr "> adding $base to $nslist" #puts stderr "> adding $base to $nslist"
set nslist [list $base {*}$nslist] set nslist [list $base {*}$nslist]
} }
if {$has_globchars} {
if {$allbelow} { if {$allbelow} {
foreach ns $nslist { foreach ns $nslist {
if {[globmatchns ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} { if {[globmatchns ${ns_absolute} $ns] || [globmatchns [nsjoin ${ns_absolute} **] $ns]} {
@ -524,6 +555,18 @@ namespace eval punk::ns {
} else { } else {
set nslist_filtered [struct::list::Lfilter $nslist [list globmatchns ${ns_absolute}]] 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_filtered
} }
return $nslist return $nslist
@ -805,20 +848,31 @@ namespace eval punk::ns {
proc nslist {{glob "*"} args} { proc nslist {{glob "*"} args} {
set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $glob]] set ns_absolute [uplevel 1 [list ::punk::ns::nspath_here_absolute $glob]]
if {[dict exists $args -match]} { 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" error "nslist requires positional argument 'glob' instead of -match option"
} }
set defaults [dict create\ set defaults [dict create\
-match $ns_absolute -match $ns_absolute\
-nspathcommands 0\
] ]
package require textblock package require textblock
set opts [dict merge $defaults $args] 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] set with_results [list]
foreach nsdict $ns_matches { foreach nsdict $ns_matches {
if {[dict get $nsdict itemcount]>0} { if {[dict get $nsdict itemcount]>0} {
lappend with_results $nsdict 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 count_with_results [llength $with_results]
set output "" set output ""
foreach nsdict $with_results { 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 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]]} { 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 {[dict size [dict get $nsdict namespacepath]]} {
if {$count_with_results > 1 } { 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] 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 #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 #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} { proc get_ns_dicts {fq_glob args} {
#JMN
#puts stderr "get_ns_dicts $fq_glob" #puts stderr "get_ns_dicts $fq_glob"
set glob_is_absolute [expr {[string match ::* $fq_glob]}] set glob_is_absolute [expr {[string match ::* $fq_glob]}]
if {!$glob_is_absolute} { if {!$glob_is_absolute} {
error "get_ns_dicts requires fully-qualified namespace glob e.g ::*" error "get_ns_dicts requires fully-qualified namespace glob e.g ::*"
} }
set has_globchars [regexp {[*?]} $fq_glob]
set defaults [dict create\ set defaults [dict create\
-allbelow 0\ -allbelow 0\
-nspathcommands 1\
] ]
set opts [dict merge $defaults $args] set opts [dict merge $defaults $args]
# -- --- --- --- --- --- --- --- --- --- --- --- # -- --- --- --- --- --- --- --- --- --- --- ---
set allbelow [dict get $opts -allbelow] 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 location [nsprefix $fq_glob]
# set glob [nstail $fq_glob] set commands [list]
# set allchildren [nschildren $location] ; #only returns 1 level deeper
# set commands [.= nscommands [nsjoin ${location} $glob] |> linelist ]
set nsglob [nsprefix $fq_glob] set nsglob [nsprefix $fq_glob]
set glob [nstail $fq_glob] set glob [nstail $fq_glob]
@ -894,11 +964,17 @@ namespace eval punk::ns {
set nsdict_list [list] set nsdict_list [list]
foreach ch $report_namespaces { foreach ch $report_namespaces {
#puts "get_ns_dicts>>> $ch glob:'$glob'" #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. #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 {} # 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 #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. #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 #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 location $ch
set exportpatterns [namespace eval $location {::namespace export}] 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 exportpatterns [nseval $location {::namespace export}]
set allexported [list] set allexported [list]
set matched [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) #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 [namespace eval $location {::info procs}]
#set allprocs [nseval $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 allaliases [list]
set allensembles [list] set allensembles [list]
set allooobjects [list] set allooobjects [list]
@ -950,7 +1039,7 @@ namespace eval punk::ns {
if {[string match *:: $a]} { if {[string match *:: $a]} {
#exception for alias such as ::p::2:: so that it doesn't show up as empty string #exception for alias such as ::p::2:: so that it doesn't show up as empty string
#lappend aliases :: #lappend aliases ::
#JMN - 2023 - better to dispaly an empty string somehow #JMN - 2023 - better to display an empty string somehow
lappend aliases "" lappend aliases ""
} else { } else {
lappend aliases [nstail $a] lappend aliases [nstail $a]
@ -993,8 +1082,9 @@ namespace eval punk::ns {
} }
} }
if {$glob ne "*"} { if {$glob ne "*"} {
set tailmatches [lsearch -all -inline $tails $glob] set childtailmatches [lsearch -all -inline $childtails $glob]
set fqchildren [lmap v $tailmatches {lindex ${location}::$v}] ;#lindex without indices is fast equivalent of 'val' #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 exported [lsearch -all -inline $allexported $glob]
set procs [lsearch -all -inline $allprocs $glob] set procs [lsearch -all -inline $allprocs $glob]
#set aliases [lsearch -all -inline $allaliases $glob] #set aliases [lsearch -all -inline $allaliases $glob]
@ -1004,8 +1094,8 @@ namespace eval punk::ns {
set imported [lsearch -all -inline $allimported $glob] set imported [lsearch -all -inline $allimported $glob]
set undetermined [lsearch -all -inline $allundetermined $glob] set undetermined [lsearch -all -inline $allundetermined $glob]
} else { } else {
set tailmatches $tails set childtailmatches $childtails
set fqchildren $allchildren #set fqchildren $allchildren
set exported $allexported set exported $allexported
set procs $allprocs set procs $allprocs
#set aliases $allaliases #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 - #itemcount will overcount if we are including commands as well as procs/exported etc -
set itemcount 0 set itemcount 0
incr itemcount [llength $tailmatches] incr itemcount [llength $childtailmatches]
incr itemcount [llength $commands] incr itemcount [llength $commands]
@ -1032,8 +1122,9 @@ namespace eval punk::ns {
#definitely don't count exportpatterns #definitely don't count exportpatterns
incr itemcount [llength $undetermined] incr itemcount [llength $undetermined]
lappend nsdict_list [dict create\
children [lsort $tailmatches]\ set nsdict [dict create\
children [lsort $childtailmatches]\
commands $commands\ commands $commands\
procs $procs\ procs $procs\
exported $exported\ exported $exported\
@ -1045,9 +1136,11 @@ namespace eval punk::ns {
namespacexport $exportpatterns\ namespacexport $exportpatterns\
undetermined $undetermined\ undetermined $undetermined\
location $location\ location $location\
namespacepath $nspathdict\
glob $glob\ glob $glob\
itemcount $itemcount\ itemcount $itemcount\
] ]
lappend nsdict_list $nsdict
} }
return $nsdict_list 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] #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. #Must be no ansi when only single arg used.
#review - ansi codes will be very confusing in some scenarios! #review - ansi codes will be very confusing in some scenarios!
#todo - only output color when requested (how?) or via repltelemetry ? #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 #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 namespace_current [namespace current]
#inspect -label info_procs [info procs] #inspect -label info_procs [info procs]
@ -1111,32 +1203,19 @@ namespace eval punk::ns {
::continue ::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?) #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 { ::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 \ ::pipecase \
caseresult.= ::list $base $what |,basens/0,g/1> {namespace eval $basens [::list ::info commands $g]} caseresult.= ::list $base $what |,basens/0,g/1> {namespace eval $basens [::list ::info commands $g]}
}] }]
#lappend commandlist {*}[@@ok/result= $matchedcommands] #lappend commandlist {*}[@@ok/result= $matchedcommands]
#need to pull result from matchedcommands dict #need to pull result from matchedcommands dict
#set cmd_tails [@@ok/result= $matchedcommands |> {::lmap v $data {punk::ns::nstail $v}}] #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 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}] ::set all_ns_tails [::lmap v $all_ns_commands {::punk::ns::nstail $v}]
::foreach c $cmd_tails { ::foreach c $cmd_tails {
::if {$c in $all_ns_tails} { ::if {$c in $all_ns_tails} {
::if {$do_raw} { ::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] ::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) #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. #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| } |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.= { interp alias {} nscommands1 {} .= ,'ok'@0.= {
set commandns [namespace current] set commandns [namespace current]
#upvar caseresult caseresult #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 ""}} { proc nsimport_noclobber {pattern {ns ""}} {
set source_ns [namespace qualifiers $pattern] set source_ns [namespace qualifiers $pattern]
if {![namespace exists $source_ns]} { if {![namespace exists $source_ns]} {
@ -1307,6 +1637,8 @@ namespace eval punk::ns {
return $imported_commands return $imported_commands
} }
#todo - use ns::nsimport_noclobber instead
interp alias {} nsthis {} punk::ns::nspath_here_absolute 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 {} nsorigin {} apply {ns {namespace origin [uplevel 1 ::punk::ns::nspath_here_absolute $ns]} ::}
interp alias {} nsvars {} punk::ns::nsvars interp alias {} nsvars {} punk::ns::nsvars
@ -1320,16 +1652,21 @@ namespace eval punk::ns {
interp alias {} nslist_dict {} punk::ns::nslist_dict interp alias {} nslist_dict {} punk::ns::nslist_dict
#extra slash implies more verbosity (ie display commands instead of just nschildren) #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 {} n// {} punk::ns::ns/ // interp alias {} n// {} punk::ns::ns/ //
interp alias {} ::/ {} punk::ns::nsup/ / interp alias {} n/// {} punk::ns::ns/ ///
interp alias {} ::// {} punk::ns::nsup/ // interp alias {} n/new {} punk::ns::n/new
interp alias {} nn/ {} punk::ns::nsup/ / interp alias {} nn/ {} punk::ns::nsup/ /
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 {} :/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 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 #as the tilde hasn't been normalized.. we can't assume we're running on the actual platform
return ~/.. return ~/..
} }
return [file dirname [winpath $path]] return [file dirname [towinpath $path]]
} }
#REVIEW high-coupling #REVIEW high-coupling
proc cdwin {path} { proc cdwin {path} {
set path [winpath $path] set path [towinpath $path]
if {$::repl::running} { if {$::repl::running} {
repl::term::set_console_title $path repl::term::set_console_title $path
} }
cd $path cd $path
} }
proc cdwindir {path} { proc cdwindir {path} {
set path [winpath $path] set path [towinpath $path]
if {$::repl::running} { if {$::repl::running} {
repl::term::set_console_title $path 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 {} cdwin {} punk::unixywindows::cdwin
#interp alias {} cdwindir {} punk::unixywindoes::cdwindir #interp alias {} cdwindir {} punk::unixywindoes::cdwindir
#interp alias {} towinpath {} punk::unixywindows::towinpath #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 i 0
set info [dict create] set info [dict create]
set testlist [list] 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 iteminfo [dict create]
set itemlen [string length $item] set itemlen [string length $item]
lappend testlist $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) # 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. # 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 # 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} { proc get_run_opts {arglist} {
if {[catch { if {[catch {
set callerinfo [info level -1] set callerinfo [info level -1]
@ -96,68 +97,7 @@ namespace eval shellrun {
return [list runopts $runopts cmdargs $cmdargs] 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} { proc run {args} {
set_last_run_display [list] set_last_run_display [list]
@ -732,27 +672,7 @@ namespace eval shellrun {
} }
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 {} run {} shellrun::run
interp alias {} trun {} shellrun::trun
interp alias {} sh_run {} shellrun::sh_run interp alias {} sh_run {} shellrun::sh_run
interp alias {} runout {} shellrun::runout interp alias {} runout {} shellrun::runout
interp alias {} sh_runout {} shellrun::sh_runout interp alias {} sh_runout {} shellrun::sh_runout
@ -762,7 +682,6 @@ namespace eval shellrun {
interp alias {} sh_runx {} shellrun::sh_runx interp alias {} sh_runx {} shellrun::sh_runx
interp alias {} runraw {} shellrun::runraw interp alias {} runraw {} shellrun::runraw
interp alias {} twexec {} shellrun::twapi_exec
#the shortened versions deliberately don't get pretty output from the repl #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\\"\ # "PM - Privacy Message" "\u001b^somethinghidden\033\\"\
} }
#candidate for zig/c implementation?
proc overtype::stripansi {text} { proc overtype::stripansi {text} {
variable escape_terminals ;#dict variable escape_terminals ;#dict
variable ansi_2byte_codes_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] 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} { proc overtype::strip_nonprinting_ascii {str} {
#review - some single-byte 'control' chars have visual representations e.g ETX as heart #review - some single-byte 'control' chars have visual representations e.g ETX as heart
#It is currently used for screen display width calculations #It is currently used for screen display width calculations
@ -230,7 +238,7 @@ proc overtype::printing_length {line} {
} }
} }
set line2 [join $outchars ""] set line2 [join $outchars ""]
return [string_columns $line2] return [punk::char::string_width $line2]
} }
proc overtype::string_columns {text} { proc overtype::string_columns {text} {
@ -262,7 +270,7 @@ proc overtype::left {args} {
if {[llength $args] < 2} { if {[llength $args] < 2} {
error {usage: ?-transparent [0|1]? ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} 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\ set defaults [dict create\
-ellipsis 0\ -ellipsis 0\
-ellipsistext $default_ellipsis_horizontal\ -ellipsistext $default_ellipsis_horizontal\
@ -322,7 +330,7 @@ proc overtype::left {args} {
} }
namespace eval |> { namespace eval overtype::piper {
proc overcentre {args} { proc overcentre {args} {
if {[llength $args] < 2} { if {[llength $args] < 2} {
error {usage: ?-bias left|right? ?-transparent [0|1|<regexp>]? ?-exposed1 <char>? ?-exposed2 <char>? ?-overflow [1|0]? overtext pipelinedata} 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} { if {[llength $args] < 2} {
error {usage: ?-start <int>? ?-transparent [0|1|<regexp>]? ?-overflow [1|0]? undertext overtext} 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} { if {[string first \n $under] >=0 || [string first \n $over] >= 0} {
error "overtype::renderline not allowed to contain newlines" error "overtype::renderline not allowed to contain newlines"
} }
@ -588,13 +596,18 @@ proc overtype::renderline {args} {
set opt_exposed2 [dict get $opts -exposed2] set opt_exposed2 [dict get $opts -exposed2]
# -- --- --- --- --- --- --- --- --- --- --- --- # -- --- --- --- --- --- --- --- --- --- --- ---
#-----
#
if {[string first \t $under] >= 0} { 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 set overdata $over
if {[string first \t $over] >= 0} { 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? #ta_detect ansi and do simpler processing?
@ -613,7 +626,7 @@ proc overtype::renderline {args} {
#pt = plain text #pt = plain text
append pt_underchars $pt append pt_underchars $pt
foreach ch [split $pt ""] { foreach ch [split $pt ""] {
set width [string_columns $ch] set width [punk::char::string_width $ch]
incr i_u incr i_u
dict set understacks $i_u $u_codestack dict set understacks $i_u $u_codestack
lappend out $ch lappend out $ch
@ -695,13 +708,13 @@ proc overtype::renderline {args} {
incr idx incr idx
} elseif {($do_transparency && [regexp $opt_transparent $ch])} { } 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) #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} { if {$idx > [llength $out]-1} {
lappend out " " lappend out " "
dict set understacks $idx [list] ;#review - use idx-1 codestack? dict set understacks $idx [list] ;#review - use idx-1 codestack?
incr idx incr idx
} else { } else {
set uwidth [string_columns [lindex $out $idx]] set uwidth [punk::char::string_width [lindex $out $idx]]
if {[lindex $out $idx] eq ""} { if {[lindex $out $idx] eq ""} {
#2nd col of 2-wide char in underlay #2nd col of 2-wide char in underlay
incr idx incr idx
@ -714,7 +727,7 @@ proc overtype::renderline {args} {
incr idx incr idx
} }
} elseif {$uwidth > 1} { } elseif {$uwidth > 1} {
if {[string_columns $ch] == 1} { if {[punk::char::string_width $ch] == 1} {
#normal singlewide transparency #normal singlewide transparency
set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay set next_pt_overchar [string index $pt_overchars $idx_over+1] ;#lookahead of next plain-text char in overlay
if {$next_pt_overchar eq ""} { if {$next_pt_overchar eq ""} {
@ -737,8 +750,8 @@ proc overtype::renderline {args} {
} }
} else { } else {
#non-transparent char in overlay #non-transparent char in overlay
set owidth [string_columns $ch] set owidth [punk::char::string_width $ch]
set uwidth [string_columns [lindex $out $idx]] set uwidth [punk::char::string_width [lindex $out $idx]]
if {[lindex $out $idx] eq ""} { if {[lindex $out $idx] eq ""} {
#2nd col of 2wide char in underlay #2nd col of 2wide char in underlay
priv::render_addchar $idx $ch [dict get $overstacks $idx_over] priv::render_addchar $idx $ch [dict get $overstacks $idx_over]
@ -782,8 +795,8 @@ proc overtype::renderline {args} {
if {$opt_overflow == 0} { if {$opt_overflow == 0} {
#need to truncate to the width of the original undertext #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? #review - string_width vs printing_length here. undertext requirement to be already rendered therefore punk::char::string_width ok?
set num_under_columns [string_columns $pt_underchars] ;#plaintext underchars set num_under_columns [punk::char::string_width $pt_underchars] ;#plaintext underchars
} }
#coalesce and replay codestacks for out char list #coalesce and replay codestacks for out char list
@ -798,7 +811,7 @@ proc overtype::renderline {args} {
foreach ch $out { foreach ch $out {
append out_rawchars $ch append out_rawchars $ch
if {$opt_overflow == 0 && !$in_overflow} { 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 { } else {
#todo - check if we overflowed with a double-width char ? #todo - check if we overflowed with a double-width char ?
#store visualwidth which may be short #store visualwidth which may be short

Loading…
Cancel
Save