You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

901 lines
39 KiB

# -*- 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.
#update 2024 - while twapi is slower to load than other packages - it's not *that* bad. Earlier tests were on a machine which may have had disk issues, and made twapi's relative difference noticeable.
#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 readchild_handler {chan hpid} {
#fileevent $chan readable {}
set data [read $chan 4096]
while {![chan blocked $chan] && ![eof $chan]} {
append data [read $chan 4096]
}
puts stdout "-->$data eof:[eof $chan] chan blocked [chan blocked $chan]"
flush stdout
if {![eof $chan]} {
puts stdout "not eof $chan [fconfigure $chan] chan blocked:[chan blocked $chan]"
#fileevent $chan readable [list punk::winrun::readchild_handler $chan $hpid]
} else {
#puts "eof: waiting exit process"
set punk::winrun::waitresult [twapi::wait_on_handle $hpid -wait -1]
}
}
proc readchilderr_handler {chan} {
fileevent $chan readable {}
set data [read $chan]
puts stderr "err: $data"
flush stderr
if {![eof $chan]} {
fileevent $chan readable [list punk::winrun::readchild_handler $chan]
}
}
proc testrun {cmdline} {
#twapi::create_file to redirect?
package require twapi
set cmdid [clock millis]
set childout [twapi::namedpipe_server [string map [list %id% $cmdid ] {//./pipe/tcl-stdout-%id%}] -access write]
set childerr [twapi::namedpipe_server [string map [list %id% $cmdid ] {//./pipe/tcl-stderr-%id%}] -access write]
set childin [twapi::namedpipe_server [string map [list %id% $cmdid ] {//./pipe/tcl-stdin-%id%}] -access read]
set psinfo [twapi::create_process {} -cmdline $cmdline -returnhandles 1 -detached 0 -newconsole 1 -showwindow hidden -inherithandles 1 -stdchannels [list $childin $childout $childerr]]
puts stdout "psinfo:$psinfo"
lassign $psinfo _pid _tid hpid htid
set readout [twapi::namedpipe_client [string map [list %id% $cmdid ] {//./pipe/tcl-stdout-%id%}] -access read]
set readerr [twapi::namedpipe_client [string map [list %id% $cmdid ] {//./pipe/tcl-stderr-%id%}] -access read]
set writein [twapi::namedpipe_client [string map [list %id% $cmdid ] {//./pipe/tcl-stdin-%id%}] -access write]
#after 1000
chan configure $readout -blocking 0
fileevent $readout readable [list readchild_handler $readout $hpid]
puts stdout "input: [chan configure $writein]"
puts $writein "puts stdout blah;"
flush $writein
puts $writein "flush stdout"
flush $writein
puts $writein "puts exiting"
puts $writein "after 10;exit 4"
flush $writein
#puts stdout x--[read $readout]
#if the cmdline is a pipeline - the wait will return as soon as the first process returns... not the entire pipeline. :/
#set waitresult [twapi::wait_on_handle $hpid -wait -1]
#e.g timeout, signalled
close $childout
close $childerr
close $childin
#after 1 [list wait_on $hpid]
variable waitresult
vwait punk::winrun::waitresult
if {$waitresult eq "timeout"} {
puts stderr "tw_run: timeout waiting for process"
}
fileevent $readout readable {}
fileevent $readerr readable {}
set code [twapi::get_process_exit_code $hpid]
twapi::close_handle $htid
twapi::close_handle $hpid
return [dict create exitcode $code]
}
proc wait_on {hpid} {
set punk::winrun::waitresult [twapi::wait_on_handle $hpid -wait -1]
}
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
#assertion - 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
#
#NOTE: used by flagfilter for splitting dispatchrecord raw element
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 essentially 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 problems 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 cmd.exe's /v flag 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