Browse Source

punk-multishell fixes - 512byte batch script label boundaries

master
Julian Noble 11 months ago
parent
commit
9b21d3b4f3
  1. 293
      src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm
  2. 293
      src/modules/punk/mix/templates/utility/scriptappwrappers/punk-multishell.cmd

293
src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm

@ -97,6 +97,280 @@ namespace eval punk::mix::commandset::scriptwrap {
return $table return $table
} }
proc range_spans_512_boundaries {start end} {
#todo - something more elegant
set is_span 0
set lowmult [expr {$start / 512}]
set highmult [expr {$end / 512}]
set test_boundaries [list]
for {set bindex $lowmult} {$bindex <= ($highmult + 1)} {incr bindex} {
lappend test_boundaries [expr {$bindex * 512}]
}
set lowboundary unset
set highboundary unset
foreach t $test_boundaries {
if {$lowboundary eq "unset"} {
if {$start <= $t } {
set lowboundary $t
}
}
if {$end >= $t} {
set highboundary $t
}
}
set boundaries [list]
foreach b $test_boundaries {
if {$lowboundary <= $b && $highboundary >= $b} {
lappend boundaries $b
}
}
if {[llength $boundaries]} {
set is_span 1
}
if {$is_span} {
return [list is_span 1 boundaries $boundaries]
} else {
return [list is_span 0]
}
}
#A batch file with unix line-endings is sensitive to label positioning.
#batch file with windows crlf line endings can exhibit this problem - but probably only if specifically crafted with long lines deliberately designed to trigger it.
#see: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 (Call and goto may fail when the batch file has Unix line endings)
#The windows batch file scanner appears to parse in 512 Byte chunks.
#If a label following a call/goto is at a position spanning a 512 byte block as counted from the call/goto site (callsite assumed to be EOL - works for basic cases at least) then the label won't be found.
#A label preceding a call/goto site can't span a 512 byte boundary as counted from the beginning of the file
#checkoutput produces warnings and errors in ansi-coloured form (both to stdout and a summary in the return value)
#The script should then be adjusted with comments and/or whitespace and checkoutput should be re-run to confirm there are no new boundary-spanning labels.
#checkoutput needs to be run even on previously tested scriptwrapper templates because the final :exit label is beyond the payloads and so could span a 512 Byte block
#It is more likely to catch issues if adjustments are made to the initial batch-script code in a template.
proc checkoutput {filepath args} {
if {![file exists $filepath]} {
error "punk::mix::commandset:scriptwrap error cannot find file '$filepath'"
}
set defaults [dict create\
-ignore_rems 0\
]
set opts [dict merge $defaults $args]
set opt_ignore_rems [dict get $opts -ignore_rems]
set filedata [fcat $filepath]
if {$opt_ignore_rems} {
set data ""
set skipped_rems 0
foreach ln [split $filedata \n] {
set ln [string trim $ln]
if {[string match @REM* $ln] || [string match REM* $ln] || [string match @rem* $ln] || [string match rem* $ln]} {
#ignore
incr skipped_rems
} else {
append data $ln \n
}
}
puts stderr "Skipped $skipped_rems rem lines"
} else {
set data $filedata
}
set lines_before_after 3
set dsize [string length $data]
puts stdout "examining $dsize bytes of file $filepath"
puts "checking 512 byte boundaries from call sites - displaying $lines_before_after lines before and after"
set result ""
set file_offset 0
set linenum 0
set error_labels [list]
set warning_labels [list]
set file_lines [split $data \n]
foreach ln $file_lines {
set callposn -1
incr linenum
set trimln [string trim $ln]
if {[string match "rem *" $trimln] || [string match "@rem *" $trimln] || [string match "REM *" $trimln] || [string match "@REM *" $trimln]} {
#ignore things that look like a call that are beind a REM
} else {
foreach search_regex [list {(.*\s+|^)(@*call\s*:)(\S.*)} {(.*\s+|^)(@*CALL\s*:)(\S.*)} {(.*\s+|^)(@*goto\s*:)(\S.*)} {(.*\s+|^)(@*GOTO\s*:)(\S.*)}] {
if {[regexp $search_regex $ln _m precall call labelplus]} {
set callposn [expr {$file_offset + [string length $ln]}] ;#take callposn as end of line .. review - multiline statements?
break
}
}
if {$callposn != -1} {
puts stdout "[a+ bold cyan]Found callsite '${call}${labelplus}' at byte $callposn line_num:$linenum line: $ln[a]"
set labelpluswords [regexp -inline -all {\S+} $labelplus] ;#don't assume labelplus can be treated as Tcl list - use regexp to split
set label [lindex $labelpluswords 0]
set labelsize [string length $label]
#scan forward for labels at boundaries
set forward_data [string range $data $callposn end]
set dsize [string length $forward_data]
set num_boundaries [expr {$dsize / 512} ]
puts "scanning $dsize forward bytes in file for labels - num_boundaries: $num_boundaries"
set scan_offset 0
set total_offset $file_offset
set found_forward_label 0
foreach scanline [split $forward_data \n] {
set line_bytes [expr {[string length $scanline] +1}] ;#+1 for unix lf
set line_start $total_offset
set line_end [expr {$total_offset + $line_bytes}]
set trimscanline [string trim $scanline]
if {[string match ":$label*" $trimscanline]} {
incr found_forward_label
set label_posn_in_line [string first : $scanline]
set labelposn [expr {$scan_offset + $label_posn_in_line}]
#we only really care about exactly landing on a boundary or else the next 512 byte boundaries above the labelposn
if {($labelposn % 512) == 0} {
set ubound [expr {($labelposn / 512) * 512}]
} else {
set ubound [expr {(($labelposn / 512)+1) * 512}]
}
set lbound [expr {$ubound - $labelsize}]
if {($labelposn >= $lbound) && ($labelposn <= $ubound)} {
lappend error_labels [list label $label call_offset_bytes $labelposn callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold red]ERROR: label $trimscanline at offset from callsite: $labelposn total offset: $total_offset[a]"
puts stdout "[a+ bold red] This label appears to span the 512byte boundary at byte $ubound[a] [a+ yellow bold]from callsite[a]"
} else {
puts stdout "[a+ bold green]OK: label $trimscanline at offset from callsite: $labelposn total offset: $total_offset[a]"
}
}
set forward_spaninfo [range_spans_512_boundaries $line_start $line_end]
if {[dict get $forward_spaninfo is_span]} {
set boundaries [dict get $forward_spaninfo boundaries]
puts stdout "line $linenum scan from call label $label at $callposn boundaries crossed: $boundaries"
}
incr total_offset $line_bytes
incr scan_offset $line_bytes
}
#scan behind for labels at boundaries - using offset from start of file
#we do a backward scan even if a forward label has been found, so that we can warn of duplicate labels.
set prior_lines [lrange $file_lines 0 $linenum] ;#only scan from file start to call-site
set prior_total_offset 0
set found_backward_label 0
set p_linenum 0
foreach pline $prior_lines {
incr p_linenum
set pline_bytes [expr {[string length $pline] +1}] ;#+1 for unix lf
set pline_start $prior_total_offset
set pline_end [expr {$prior_total_offset + $pline_bytes}]
set spaninfo [range_spans_512_boundaries $pline_start $pline_end]
puts stdout "line $p_linenum byte range $pline_start -> $pline_end [a+ bold purple]$spaninfo[a]"
set trimpline [string trim $pline]
if {[string match ":$label*" $trimpline]} {
incr found_backward_label
set prior_label_posn_in_line [string first : $pline]
set prior_label_posn [expr {$prior_total_offset + $prior_label_posn_in_line}]
if {($prior_label_posn % 512) == 0} {
set p_ubound [expr {($prior_label_posn / 512) * 512}]
} else {
set p_ubound [expr {(($prior_label_posn /512) +1) * 512}]
}
set p_lbound [expr {$p_ubound - $labelsize}]
if {($prior_label_posn >= $p_lbound) && ($prior_label_posn <= $p_ubound)} {
lappend error_labels [list label $label file_offset_bytes $prior_label_posn callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold red]ERROR: label '$trimpline' at line $p_linenum and offset from file start: $prior_label_posn total offset: $prior_total_offset[a]"
puts stdout "[a+ bold red] This label appears to span the 512byte boundary at byte $p_ubound[a] [a+ yellow bold]from file start[a]"
} else {
puts stdout "[a+ bold green]OK: prior label '$trimpline' at offset from file start: $prior_label_posn total offset: $prior_total_offset[a]"
}
}
if {[dict get $spaninfo is_span]} {
#check boundaries within the line
set boundaries [dict get $spaninfo boundaries]
foreach b $boundaries {
if {$b == 0} {
#skip - beginning of line already handled (review?)
continue
}
#overlap test is just a warning - we have a label-like thing overlapping the boundary
set overlaptail [string range $pline [expr {$b - $labelsize}] [expr {($b + $labelsize) -1}]] ;#subtracting labelsize gives earliest possible overlap
if {[string match "*:$label *" $overlaptail] } {
lappend warning_labels [list label $label warning label_spanning callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold yellow] WARNING: possible label $label spans boundary $b from start of file"
}
set pline_tail [string range $pline $b end]
#if {[string match ":$label *" $pline_tail]} {}
set re1 {\s*:%lbl%[\s|^|=].*}
set re1 [string map [list %lbl% $label] $re1]
set re2 {\s*:%lbl%$}
set re2 [string map [list %lbl% $label] $re2]
if {[regexp $re1 $pline_tail] || [regexp $re2 $pline_tail]} {
lappend error_labels [list label $label file_offset_bytes $b note "label at boundary but no preceeding newline - cmd may interpret as label and execute following line or code at next boundary" callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold red]ERROR: *possible* label '$label' at line $p_linenum and offset from file start: $b total offset: $prior_total_offset[a]"
puts stdout "[a+ bold red] This label with no preceeding newline appears to span the 512byte boundary at byte $b[a] [a+ yellow bold]from file start[a]"
puts stdout "[a+ bold yellow] label starting at $b : $pline_tail[a]"
set tail_start $b
set tail_end [expr {$b + [string length $pline_tail]}]
set tail_spaninfo [range_spans_512_boundaries $tail_start $tail_end]
if {[dict get $tail_spaninfo is_span]} {
set tail_boundaries [dict get $tail_spaninfo boundaries]
set extra_tail_boundaries [lsearch -all -inline -not $tail_boundaries $b]
if {[llength $extra_tail_boundaries]} {
puts "Line $p_linenum also spans additional boundaries: $extra_tail_boundaries"
set next_boundary [lindex $extra_tail_boundaries 0]
set next_boundary_data [string range $pline [expr {$prior_total_offset + $next_boundary}] end]
puts "Line $p_linenum data at next boundary: [a+ yellow bold]$next_boundary_data[a]"
puts "[a+ yellow bold]NOTE: cmd may attempt to treat this data as code[a]"
}
} else {
set nextline [lindex $file_lines $pline_tail+1]
puts "Line $p_linenum + 1 has data: [a+ yellow bold]$nextline[a]"
puts "[a+ yellow bold]NOTE: cmd may attempt to treat this data as code[a]"
}
}
}
}
incr prior_total_offset $pline_bytes
}
if {$found_forward_label == 0} {
if {[string toupper $label] eq "EOF"} {
#EOF/eof label is special - it doesn't have to exist - but if it does - it probably shouldn't be on a boundary
puts stdout "[a+ bold green]OK: label :$label doesn't exist - but it's not required. callsite: [list call ${call}${labelplus} call_linenum $linenum] [a]"
} else {
if {$found_backward_label == 0} {
lappend warning_labels [list label $label warning label_not_found callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold yellow]WARNING: label :$label not found (in forward or backward scan)[a]"
}
}
}
if {($found_forward_label + $found_backward_label) > 1} {
lappend warning_labels [list label $label warning multiple_labels_found callsite [list call ${call}${labelplus} call_linenum $linenum]]
puts stdout "[a+ bold yellow]WARNING: label :$label seems to appear multiple times[a]"
}
}
}
incr file_offset [string length $ln]
incr file_offset ;# for unix nl
}
if {[llength $warning_labels]} {
append result "WARNING:" \n
append result "The following labels had warnings" \n
foreach w $warning_labels {
append result " [a+ bold yellow]$w[a]" \n
}
}
if {[llength $error_labels]} {
append result "ERROR: label location errors found" \n
append result "The following labels appear to span 512 Byte boundaries or occur on boundaries without a preceding newline and are likely to cause batch script errors" \n
append result "For labels spanning boundaries the label is likely to be missed by the batch interpreter" \n
append result "For labels occuring at boundaries but not at the beginning of a line, the label may be interpreted as a label when not expected, and the interpreter may run code on next line or next boundary" \n
append result "Try adding comments and/or spacing between the call site at the call_lineum indicated and the label and then re-test in case there are further boundary collisions" \n
foreach err $error_labels {
append result " [a+ bold red]$err[a]" \n
}
}
return $result
}
#specific filepath to just wrap one script at the tcl-payload or xxx-payload-pre-tcl site #specific filepath to just wrap one script at the tcl-payload or xxx-payload-pre-tcl site
#scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #scriptset name to substiture multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf
proc multishell {filepath_or_scriptset args} { proc multishell {filepath_or_scriptset args} {
@ -436,12 +710,27 @@ namespace eval punk::mix::commandset::scriptwrap {
puts -nonewline $fdtarget $newscript puts -nonewline $fdtarget $newscript
close $fdtarget close $fdtarget
puts stdout "Wrote script file at $output_file" puts stdout "Wrote script file at $output_file"
set check_result [checkoutput $output_file]
set with_errors ""
set with_warnings ""
if {$check_result ne ""} {
puts stdout $check_result
set check_lines [split $check_result \n]
foreach cl $check_lines {
set trimcl [string trim $cl]
if {[string match "ERROR:*" $trimcl]} {
set with_errors "[a+ bold red]with errors[a]"
}
if {[string match "WARNING:*" $trimcl]} {
set with_warnings "[a+ bold yellow] with warnings[a]"
}
}
}
#even though chmod might exist on windows - we will leave permissions alone #even though chmod might exist on windows - we will leave permissions alone
if {$::tcl_platform(platform) ne "windows"} { if {$::tcl_platform(platform) ne "windows"} {
catch {exec chmod +x $output_file} catch {exec chmod +x $output_file}
} }
puts stdout "-done-" puts stdout "-done- $with_errors $with_warnings"
return $output_file return $output_file
} }

293
src/modules/punk/mix/templates/utility/scriptappwrappers/punk-multishell.cmd

@ -1,24 +1,62 @@
: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^ : "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^
set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; Hide :;Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @'
: heredoc1 - hide from powershell using @ and squote above. (close sqote for unix shells) ' \ : heredoc1 - hide from powershell using @ and squote above. (close sqote for unix shells) ' \
: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \
: "[Hide @ECHO; Hide ); Hide (;Hide echo; Hide @REM]#not necessary but can help avoid errs in testing" : "[Hide @ECHO; Hide ); Hide (;Hide echo; Hide @REM]#not necessary but can help avoid errs in testing"
: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' : << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH'
: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : 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. : STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required on unix or windows and will reduce functionality and/or portability.
@REM Even comment lines can be part of the functionality of this script - modify with care. : Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care.
@REM Change the value of nextshell in the next line if desired, and code within payload sections as appropriate. @REM ############################################################################################################################
@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, sh and/or powershelll (powershell.exe or pwsh.exe)
@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained.
@REM ############################################################################################################################
@REM On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate.
@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system
@REM e.g from within a running punkshell: pmix scriptwrap.multishell <inputfilepath> -outputfolder <folderpath>
@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used)
@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called.
@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only.
@SET "validshells=pwsh,sh,bash,tclsh"
@SET shells[10]="pwsh"
@SET shells[11]="sh"
@set shells[12]="bash"
@SET shells[13]="tclsh"
: <nextshell>
@SET "nextshell=tclsh" @SET "nextshell=tclsh"
: </nextshell>
@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable).
: <asadmin>
@SET "asadmin=0"
: </asadmin>
@REM nextshell set to pwsh,sh,bash or tclsh @REM nextshell set to pwsh,sh,bash or tclsh
@REM @ECHO nextshell is %nextshell% @REM @ECHO nextshell is %nextshell%
@SET "validshells=pwsh,sh,bash,tclsh"
@CALL SET keyRemoved=%%validshells:%nextshell%=%% @CALL SET keyRemoved=%%validshells:%nextshell%=%%
@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available @REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
@REM -- cmd/batch file section (ignored on unix) @REM -- cmd/batch file section (ignored on unix)
@REM -- This section intended only to launch the next shell @REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary)
@REM -- Avoid customising this if possible. cmd/batch script is probably the least expressive language and most error prone. @REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script is useful, but is probably the least expressive language and most error prone.
@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888
@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly.
@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133
@REM ############################################################################################################################
@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway)
@REM -- Even something as simple as adding or removing an @REM
@REM -- From within punkshell - use:
@REM -- pmix scriptwrap.checkoutput <filepath>
@REM -- to check your templates or final wrapped scripts for byte boundary issues
@REM -- It will report any labels that are on boundaries
@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets.
@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using pmix scriptwrap.checkoutput is still recommended.
@REM -- Alternatively, as you should do anyway - test the final script on windows
@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations.
@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here.
@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label
@REM -- It is unknown what versions of cmd interpreters behave this way - and pmix scriptwrap.checkoutput doesn't check all such boundaries.
@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided
@REM ############################################################################################################################
@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections @REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections
@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ###
@SETLOCAL EnableExtensions EnableDelayedExpansion @SETLOCAL EnableExtensions EnableDelayedExpansion
@ -26,7 +64,59 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe
@SET "fname=%~nx0" @SET "fname=%~nx0"
@REM @ECHO fname %fname% @REM @ECHO fname %fname%
@REM @ECHO winpath %winpath% @REM @ECHO winpath %winpath%
@SET pwshtest_exitcode=99 @REM @ECHO commandlineascalled %0
@REM @ECHO commandlineresolved %~f0
@CALL :getNormalizedScriptTail nftail
@ECHO normalizedscripttail %nftail%
@CALL :getFileTail %0 clinetail
@ECHO clinetail %clinetail%
@CALL :stringToUpper %~nx0 capscripttail
@ECHO capscriptname: %capscripttail%
@CALL :isNumeric "blah"
@CALL :isNumeric etc
@CALL :isNumeric 3
@CALL :isNumeric 6
@IF %nftail%==%capscripttail% (
@ECHO forcing asadmin=1 due to file name on filesystem being uppercase
@SET "asadmin=1"
) else (
@CALL :stringToUpper %clinetail% capcmdlinetail
@ECHO capcmdlinetail %capcmdlintetail%
IF %clinetail%==%capcmdlinetail% (
@ECHO forcing asadmin=1 due to cmdline scriptname in uppercase
@set "asadmin=1"
)
)
@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs"
@SET arglist=%*
@IF !asadmin!==1 (
net file 1>NUL 2>NUL
@IF '!errorlevel!'=='0' ( GOTO gotPrivileges ) else ( GOTO getPrivileges )
)
@GOTO skip_privileges
:getPrivileges
@IF '%1'=='PUNK-ELEVATED' (echo PUNK-ELEVATED & shift /1 & goto gotPrivileges)
@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO UAC.ShellExecute "%~dp0%~n0.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script in new windows due to administrator elevation
@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
@EXIT /B
:gotPrivileges
@REM setlocal & pushd .
@PUSHD .
@cd /d %~dp0
@IF '%1'=='PUNK-ELEVATED' (
@DEL "%vbsGetPrivileges%" 1>nul 2>nul
@SET arglist=%arglist:~14%
)
:skip_privileges
@SET need_ps1=0 @SET need_ps1=0
@REM we want the ps1 to exist even if the nextshell isn't powershell @REM we want the ps1 to exist even if the nextshell isn't powershell
@if not exist "%~dp0%~n0.ps1" ( @if not exist "%~dp0%~n0.ps1" (
@ -42,50 +132,43 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe
) )
:pscontinue :pscontinue
@IF !need_ps1!==1 ( @IF !need_ps1!==1 (
CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL
SET pwshtest_exitcode=!errorlevel!
if !pwshtest_exitcode!==0 (
CALL pwsh -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned
COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL
) else (
CALL powershell -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned
COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL
) )
) @REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
@IF %nextshell%==pwsh ( @IF %nextshell%==pwsh (
if !pwshtest_exitcode!==99 (
REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time
REM test availability of preferred option of powershell7+ pwsh REM test availability of preferred option of powershell7+ pwsh
CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; write-host "statusmessage: pwsh-found" >NUL
SET pwshtest_exitcode=!errorlevel! SET pwshtest_exitcode=!errorlevel!
REM ECHO pwshtest_exitcode !pwshtest_exitcode! REM ECHO pwshtest_exitcode !pwshtest_exitcode!
)
REM fallback to powershell if pwsh failed REM fallback to powershell if pwsh failed
IF !pwshtest_exitcode!==0 ( IF !pwshtest_exitcode!==0 (
CALL pwsh -nop -nol "%~dp0%~n0.ps1" %* & SET task_exitcode=!errorlevel! pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel!
) ELSE ( ) ELSE (
REM CALL powershell -nop -nol -c write-host powershell-found REM CALL powershell -nop -nol -c write-host powershell-found
CALL powershell -nop -nol -file "%~dp0%~n0.ps1" %* REM powershell -nop -nol -file "%~dp0%~n0.ps1" %*
powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist%
SET task_exitcode=!errorlevel! SET task_exitcode=!errorlevel!
) )
) ELSE ( ) ELSE (
IF %nextshell%==bash ( IF %nextshell%==bash (
CALL :getWslPath %winpath% wslpath CALL :getWslPath %winpath% wslpath
REM ECHO wslfullpath "!wslpath!%fname%" REM ECHO wslfullpath "!wslpath!%fname%"
CALL %nextshell% "!wslpath!%fname%" %* & SET task_exitcode=!errorlevel! %nextshell% "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel!
) ELSE ( ) ELSE (
REM probably tclsh or sh REM probably tclsh or sh
IF NOT "x%keyRemoved%"=="x%validshells%" ( 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 sh on windows 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 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! %nextshell% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel!
) ELSE ( ) ELSE (
ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells% ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells%
SET task_exitcode=66 SET task_exitcode=66
GOTO :exit GOTO :exit_multishell
) )
) )
) )
@REM batch file library functions
@GOTO :endlib @GOTO :endlib
:getWslPath :getWslPath
@SETLOCAL @SETLOCAL
@ -101,14 +184,148 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe
ECHO %result% ECHO %result%
) )
) )
@GOTO :eof @EXIT /B
:endlib
:getFileTail
@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd
@REM we can't use things such as %~nx1 as it can change capitalisation
@REM This function is designed explicitly to preserve capitalisation
@REM accepts full paths with either / or \ as delimiters - or
@SETLOCAL
@SET "rtrn=%~2"
@SET "arg=%~1"
@REM @SET "result=%_arg:*/=%"
@REM @SET "result=%~1"
@SET LF=^
: The above 2 empty lines are important. Don't remove
@CALL :stringContains "!arg!" "\" hasBackSlash
@IF "!hasBackslash!"=="true" (
@for %%A in ("!LF!") do @(
@FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B"
)
) ELSE (
@CALL :stringContains "!arg!" "/" hasForwardSlash
@IF "!hasForwardSlash!"=="true" (
@FOR %%A in ("!LF!") do @(
@FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B"
)
) ELSE (
@set "result=%arg%"
)
)
@ENDLOCAL & (
@if "%~2" neq "" (
@SET "%rtrn%=%result%"
) ELSE (
@ECHO %result%
)
)
@EXIT /B
:getNormalizedScriptTail
@SETLOCAL
@SET "result=%~nx0"
@SET "rtrn=%~1"
@ENDLOCAL & (
@IF "%~1" neq "" (
@SET "%rtrn%=%result%"
) ELSE (
@ECHO %result%
)
)
@EXIT /B
:getNormalizedFileTailFromPath
@REM warn via echo, and do not set return variable if path not found
@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized'
@SETLOCAL
@CALL :stringContains %~1 "\" hasBackSlash
@CALL :stringContains %~1 "/" hasForwardSlash
@IF "%hasBackslash%-%hasForwardslash%"=="false-false" (
@SET "P=%cd%%~1"
@CALL :getNormalizedFileTailFromPath "!P!" ftail2
@SET "result=!ftail2!"
) else (
@IF EXIST "%~1" (
@SET "result=%~nx1"
) else (
@ECHO error getNormalizedFileTailFromPath file not found: %~1
@EXIT /B 1
)
)
@SET "rtrn=%~2"
@ENDLOCAL & (
@IF "%~2" neq "" (
SET "%rtrn%=%result%"
) ELSE (
@ECHO getNormalizedFileTailFromPath %1 result: %result%
)
)
@EXIT /B
:stringContains
@REM usage: @CALL:stringContains string needle returnvarname
@SETLOCAL
@SET "rtrn=%~3"
@SET "string=%~1"
@SET "needle=%~2"
@IF "!string:%needle%=!"=="!string!" @(
@SET "result=false"
) ELSE (
@SET "result=true"
)
@ENDLOCAL & (
@IF "%~3" neq "" (
@SET "%rtrn%=%result%"
) ELSE (
@ECHO stringContains %string% %needle% result: %result%
)
)
@EXIT /B
:stringToUpper
@SETLOCAL
@SET "rtrn=%~2"
@SET "string=%~1"
@SET "capstring=%~1"
@FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @(
@SET "capstring=!capstring:%%A=%%A!"
)
@SET "result=!capstring!"
@ENDLOCAL & (
@IF "%~2" neq "" (
@SET "%rtrn%=%result%"
) ELSE (
@ECHO stringToUpper %string% result: %result%
)
)
@EXIT /B
:isNumeric
@SETLOCAL
@SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i"
@IF defined notnumeric (
@SET "result=false"
) else (
@SET "result=true"
)
@SET "rtrn=%~2"
@ENDLOCAL & (
@IF "%~2" neq "" (
@SET "%rtrn%=%result%"
) ELSE (
@ECHO %result%
)
)
@EXIT /B
:endlib
: \ : \
@REM @SET taskexit_code=!errorlevel! & goto :exit @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell
@GOTO :exit @GOTO :exit_multishell
# } # }
# rem call %nextshell% "%~dp0%~n0.cmd" %*
# -*- tcl -*- # -*- tcl -*-
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### # ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- tcl script section # -- tcl script section
@ -123,8 +340,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe
# -- e.g tclsh filename.cmd # -- e.g tclsh filename.cmd
# -- # --
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### # ## ### ### ### ### ### ### ### ### ### ### ### ### ###
rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
Hide :exit;Hide {<#};Hide '@ Hide :exit_multishell;Hide {<#};Hide '@
namespace eval ::punk::multishell { namespace eval ::punk::multishell {
set last_script_root [file dirname [file normalize ${argv0}/__]] set last_script_root [file dirname [file normalize ${argv0}/__]]
set last_script [file dirname [file normalize [info script]/__]] set last_script [file dirname [file normalize [info script]/__]]
@ -268,16 +485,18 @@ Exit $LASTEXITCODE
# heredoc2 for powershell to ignore block below # heredoc2 for powershell to ignore block below
$1 = @' $1 = @'
' '
: end hide powershell-block from Tcl \ : comment end hide powershell-block from Tcl \
# This comment with closing brace should stay in place whether 'if' commented or not } # This comment with closing brace should stay in place whether 'if' commented or not }
: cmd exit label - return exitcode : multishell cmd exit label - return exitcode
:exit :exit_multishell
: \ : \
@REM @ECHO exitcode: !task_exitcode! @REM @ECHO exitcode: !task_exitcode!
: \ : \
@IF '%asadmin%'=='1' (echo. & @cmd /k echo elevated prompt: type exit to quit)
: \
@EXIT /B !task_exitcode! @EXIT /B !task_exitcode!
# cmd has exited # cmd has exited
: end heredoc2 \ : comment end heredoc2 \
'@ '@
<# <#
# id:tailblock0 # id:tailblock0

Loading…
Cancel
Save