Browse Source

update multishell scriptsapps

master
Julian Noble 6 months ago
parent
commit
6640b80303
  1. 2
      .gitignore
  2. 444
      bin/getzig.cmd
  3. 538
      bin/runtime.cmd
  4. 686
      bin/tclargs.cmd
  5. 541
      getpunk.cmd
  6. 3
      src/scriptapps/getpunk.bash
  7. 7
      src/scriptapps/getpunk_wrap.toml
  8. 7
      src/scriptapps/runtime_wrap.toml

2
.gitignore vendored

@ -1,4 +1,6 @@
*.lastrun
#/bin/
/bin/*
!/bin/*.cmd

444
bin/getzig.cmd

@ -95,19 +95,19 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting
@REM Supporting more explicit oses than those listed may also require script padding adjustment
: <<nextshell_start>>
@SET "nextshellpath[win32___________]=pwsh -nop -nol -ExecutionPolicy bypass -c_______________________"
@SET "nextshellpath[win32___________]=pwsh -nop -nol -ExecutionPolicy bypass -c_______________________________________________________________________________________"
@SET "nextshelltype[win32___________]=pwsh____________"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[dragonflybsd____]=bash____________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[freebsd_________]=bash____________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[netbsd__________]=bash____________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[linux___________]=bash____________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[macosx__________]=bash____________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[other___________]=bash____________"
: <<nextshell_end>>
@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).
@ -119,7 +119,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed
@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed%
@SET "selected_shellpath=%nextshellpath[win32___________]%"
@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed
@CALL :stringTrimTrailingUnderscores "%selected_shellpath%" selected_shellpath_trimmed
@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%"
@REM @ECHO keyremoved %keyRemoved%
@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
@ -151,6 +151,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET "winpath=%~dp0" %= e.g c:\punkshell\bin\ %=
@SET "fname=%~nx0"
@SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =%
@SET "fullscriptname=%~dp0%~n0%~x0"
@REM @ECHO fname %fname%
@REM @ECHO winpath %winpath%
@REM @ECHO commandlineascalled %0
@ -162,6 +163,62 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringToUpper %~nx0 capscripttail
@REM @ECHO capscriptname: %capscripttail%
@goto skip_parameter_wrangling
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args and shift, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
:skip_parameter_wrangling
@IF "%nftail%"=="%capscripttail%" (
@ECHO forcing asadmin=1 due to file name on filesystem being uppercase
@SET "asadmin=1"
@ -189,31 +246,112 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
:getPrivileges
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@ECHO pre = "/c %fullscriptname% PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@REM @echo pre = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@echo args = pre >> "%vbsGetPrivileges%"
@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
@ECHO args = args ^& Chr(34) ^& strArg ^& Chr(34) ^& " " >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script in new window due to administrator elevation
@GOTO skiptest
%= Option Explicit =%
%= We need a child process to locate the current script. =%
@ECHO Const FLAG_PROCESS = "winver.exe" >> "%vbsGetPrivileges%"
%= ' WMI constants %=
@ECHO Const wbemFlagForwardOnly = 32 >> "%vbsGetPrivileges%"
%=' Generate a unique value to be used as a flag =%
@ECHO Dim guid >> "%vbsGetPrivileges%
@ECHO guid = Left(CreateObject("Scriptlet.TypeLib").GUID,38) >> "%vbsGetPrivileges%"
%= ' Start a process using the indicated flag inside its command line =%
@ECHO WScript.CreateObject("WScript.Shell").Run """" ^& FLAG_PROCESS ^& """ " ^& guid, 0, False >> "%vbsGetPrivileges%"
%= ' To retrieve process information a WMI reference is needed =%
@ECHO Dim wmi >> "%vbsGetPrivileges%"
@ECHO Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}^!\\.\root\cimv2") >> "%vbsGetPrivileges%"
%= ' Query the list of processes with the flag in its command line, retrieve the =%
%= ' process ID of its parent process ( our script! ) and terminate the process =%
@ECHO Dim colProcess, process, myProcessID >> "%vbsGetPrivileges%"
@ECHO Set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT ParentProcessID From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE Name='" ^& FLAG_PROCESS ^& "' " ^& _>> "%vbsGetPrivileges%"
@ECHO "AND CommandLine LIKE '%%" ^& guid ^& "%%'" _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO myProcessID = process.ParentProcessID >> "%vbsGetPrivileges%"
@ECHO process.Terminate >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
%= ' Knowing the process id of our script we can query the process list =%
%= ' and retrieve its command line =%
@ECHO Dim commandLine >> "%vbsGetPrivileges%"
@ECHO set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT CommandLine From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE ProcessID=" ^& myProcessID _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO commandLine = process.CommandLine >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO WScript.Echo "raw commandline: " ^& commandLine >>"%vbsGetPrivileges%"
%= ' Done =%
@ECHO intpos = 0 >> "%vbsGetPrivileges%"
@ECHO intCount = 0 >> "%vbsGetPrivileges%"
@ECHO intstartsearch = 1 >> "%vbsGetPrivileges%"
@ECHO intmax = 100 >> "%vbsGetPrivileges%"
@ECHO do While intCount ^< 4 and intmax ^> 0 >> "%vbsGetPrivileges%"
@ECHO intpos = InStr(intstartsearch, commandline, """") >> "%vbsGetPrivileges%"
@ECHO if intpos ^<^> 0 then >> "%vbsGetPrivileges%"
@ECHO intCount = intCount + 1 >> "%vbsGetPrivileges%"
@ECHO if intcount = 4 then >> "%vbsGetPrivileges%"
@ECHO ' wscript.echo "position: " ^& intpos >> "%vbsGetPrivileges%"
@ECHO commandline = Mid(commandline,intpos+1) >> "%vbsGetPrivileges%"
@ECHO exit do >> "%vbsGetPrivileges%"
@ECHO else >> "%vbsGetPrivileges%"
@ECHO intstartsearch = intpos + 1 >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO intmax = intmax -1 >> "%vbsGetPrivileges%"
@ECHO Loop >> "%vbsGetPrivileges%"
@ECHO if intcount ^< 4 then >> "%vbsGetPrivileges%"
@ECHO err.raise vbObjectError + 1001, "vbsGetPrivileges", "failed to parse commandline" >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO commandline = pre ^& commandline >> "%vbsGetPrivileges%"
@ECHO WScript.Echo "commandline: " ^& commandLine >>"%vbsGetPrivileges%"
@ECHO WScript.Echo "args: " ^& args >>"%vbsGetPrivileges%"
:skiptest
@ECHO UAC.ShellExecute "cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@REM @ECHO UAC.ShellExecute "%fullscriptname%", commandline, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script "%fullscriptname%" in new window due to administrator elevation with args: "%*"
@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
@REM @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" !newcommandline!
@EXIT /B
@REM buffer
@REM buffer
:gotPrivileges
@REM setlocal & pushd .
@PUSHD .
@cd /d %~dp0
@cd /d %winpath%
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
@DEL "%vbsGetPrivileges%" 1>nul 2>nul
@SET arglist=%arglist:~14%
@SHIFT
)
:skip_privileges
@SET need_ps1=0
@REM we want the ps1 to exist even if the nextshell isn't powershell
@if not exist "%~dp0%~n0.ps1" (
@if not exist "%scriptrootname%.ps1" (
@SET need_ps1=1
) ELSE (
fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different
fc "%fullscriptname%" "%scriptrootname%.ps1" >nul || goto different
@REM @ECHO "files same"
@SET need_ps1=0
)
@ -223,74 +361,13 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET need_ps1=1
:pscontinue
@IF !need_ps1!==1 (
COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL
COPY "%fullscriptname%" "%scriptrootname%.ps1" >NUL
)
@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
@IF "!selected_shelltype_trimmed!"=="none" (
SET selected_shelltype_trimmed=pwsh
)
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
@REM @SET "squoted_args="
@REM @for %%a in (%*) do @(
@REM set "v=%%a"
@ -311,19 +388,24 @@ SETLOCAL EnableDelayedExpansion
REM fallback to powershell if pwsh failed
IF !pwshtest_exitcode!==0 (
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist%
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted
cmd /c pwsh -nop -nol -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
@rem pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" %arglist% %= ok =%
@rem cmd /c pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
SET task_exitcode=!errorlevel!
) ELSE (
REM TODO prompt user with option to call script to install pwsh using winget
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
)
) ELSE (
IF "!selected_shelltype_trimmed!"=="powershell" (
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
@rem @echo powershell - !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist% %= ok - this works =%
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
) ELSE (
IF "!selected_shelltype_trimmed!"=="wslbash" (
@ -337,24 +419,23 @@ SETLOCAL EnableDelayedExpansion
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 The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode
@ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
%selected_shellpath_trimmed% "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
@REM @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
!selected_shellpath_trimmed! "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
) ELSE (
ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes%
SET task_exitcode=66
@REM boundary padding
GOTO :exit_multishell
)
)
)
)
@REM batch file library functions
@GOTO :endlib
@REM padding
@REM padding
@REM padding
@REM padding
%= ---------------------------------------------------------------------- =%
@rem courtesy of dbenham
@ -458,6 +539,7 @@ do if not defined param1 set %%~"param1=%2%%~"
rem %1 #%2#
@exit /b
@rem padding
@REM courtesy of: https://stackoverflow.com/users/463115/jeb
:strlen stringVar returnVar
@(
@ -506,6 +588,8 @@ do if not defined param1 set %%~"param1=%2%%~"
)
@EXIT /B
@REM padding
@REM padding
: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
@ -545,6 +629,7 @@ do if not defined param1 set %%~"param1=%2%%~"
@EXIT /B
@REM boundary padding
@REM boundary padding
@REM boundary padding
:getNormalizedScriptTail
@SETLOCAL
@SET "result=%~nx0"
@ -654,14 +739,14 @@ do if not defined param1 set %%~"param1=%2%%~"
@REM boundary padding
@REM boundary padding
@REM boundary padding
@REM boundary padding
:stringTrimTrailingUnderscores
@SETLOCAL
@SET "rtrn=%~2"
@SET "string=%~1"
@SET "trimstring=%~1"
@REM trim up to 63 underscores from the end of a string using string substitution
@REM trim up to 127 underscores from the end of a string using string substitution
@SET "trimstring=%trimstring%###"
@SET "trimstring=%trimstring:________________________________________________________________###=###%"
@SET "trimstring=%trimstring:________________________________###=###%"
@SET "trimstring=%trimstring:________________###=###%"
@SET "trimstring=%trimstring:________###=###%"
@ -707,10 +792,11 @@ do if not defined param1 set %%~"param1=%2%%~"
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- 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
# -- It is tuned to run (and possibly divert to different payload shell) when called from cmd.exe as a batch file, tclsh,sh,zsh,bash,perl or pwsh/powershell script
# -- i.e it is a polyglot file.
# -- The payload target (by os) is defined in the nextshell block at the top which is constructed when generating the polyglot
# -- using the tcl 'dev scriptwrap.multishell' command in a tcl punk shell
# -- The payload can be tcl,perl,powershell/pwsh or zsh/bash.
# -- 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 ./scriptname.cmd in sh or zsh or bash
@ -760,15 +846,65 @@ foreach ln [split $scriptdata \n] {
}
}
if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} {
set script_as_called [info script]
set script_rootname [file rootname $script_as_called]
if {$nextshelltype in "pwsh powershell"} {
set scrname [file rootname [info script]].ps1
set arglist [list]
foreach a $::argv {
set a "'$a'"
lappend arglist $a
# experimental
set script_ps1 $script_rootname.ps1
set arglist $::argv
if {[file extension $script_as_called] ne ".ps1"} {
#we need to ensure .ps1 is up to date
set needs_updating 0
if {![file exists $script_ps1]} {
set needs_updating 1
} else {
#both exist
if {[file size $script_as_called] != [file size $script_ps1]} {
set needs_updating 1
} else {
#both exist with same size - do full check that they're identical
catch {package require sha256}
if {[package provide sha256] ne ""} {
set h1 [sha2::sha256 -hex -file $script_as_called]
set h2 [sha2::sha256 -hex -file $script_ps1]
if {[string length $h1] != 64 || [string length $h2] != 64} {
set needs_updating 1
} elseif {$h1 ne $h2} {
set needs_updating 1
}
} else {
set scrname [info script]
#manually compare - scripts aren't too big, so slurp and string compare is fine
set fd [open $script_as_called]
chan configure $fd -translation binary
set data1 [read $fd]
close $fd
set fd [open $script_ps1]
chan configure $fd -translation binary
set data2 [read $fd]
close $fd
if {![string equal $data1 $data2]} {
set needs_updating 1
}
}
}
}
if {$needs_updating} {
file copy -force $script_as_called $script_ps1
}
} else {
#when called on the .ps1 - we assume it's up to date - review
}
set scrname $script_ps1
#set arglist [list]
#foreach a $::argv {
# set a "'$a'"
# lappend arglist $a
#}
} else {
set scrname $script_as_called
set arglist $::argv
}
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist"
@ -867,8 +1003,11 @@ if {[::punk::multishell::is_main]} {
HEREDOC1B_HIDE_FROM_BASH_AND_SH
# Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \
echo "var0: $0 @: $@"
# use oldschool backticks and sed - lowest common denominator \
ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
# use oldschool backticks and sed (posix - lowest common denominator) \
# ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` \
# some ps impls will return arguments - so last field not always appropriate \
# some ps impls don't have -o (e.g gnu cygwin) so ps_shellname may remain empty and emit an error \
ps_shellname=`ps -o pid,comm -p $$ | awk '$1 != "PID" {print $2}'`
# \
echo "shell from ps: $ps_shellname"
# \
@ -900,8 +1039,11 @@ pop() {
}
# ------------------------------------------------------------------------------
# non-bash-like posix diversion \
if [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ]; then
# non-bash-like posix diversion
# we don't use $BASH_VERSION/$ZSH_VERSION as these can still be set when for example
# sh is a symlink to bash (posix-mode bash - reduced bashism capabilities?)
# if our ps_shellname didn't contain a result, don't divert and risk looping
if [ -n "$ps_shellname" ] && [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ] ; then
shift
pop $#
eval "$POP_EXPR"
@ -952,18 +1094,32 @@ elif [[ "$plat" == "MINGW64"* ]]; then
elif [[ "$plat" == "CYGWIN_NT"* ]]; then
os="win32"
elif [[ "$plat" == "MSYS_NT"* ]]; then
#review..
echo MSYS
#win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#review..
#Need to consider the difference between when msys2 was launched (which strips some paths and sets up the environment)
# vs if the msys2 sh was called - (actually bash) in which case paths will be different
#wsl and cygwin or msys2 can commonly be problematic combinations - primarily due to path issues
#e.g "c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#e.g It means a /usr/bin/env bash call may launch the (linux elf) bash for wsl rather than the msys bash
#
#msys provides win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#bash reports $OSTYPE msys
#there are statements around the web that cmd /c .. will work under msys2
# - but from experience, it can be required to use cmd //c ...
# or MSYS2_ARG_CONV_ECL='*' cmd /c ..
# This seems to be because process arguments that look like unix paths are converted to windows paths :/
#review!
os="win32"
#review - need ps/sed/awk to determine shell?
interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
interp=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
#use 'command -v' (shell builtin preferred over external which)
shellpath=`command -v $interp`
shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname
#"c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#This breaks calls to various unix utils such as sed etc (wsl related?)
export PATH="$shellfolder${PATH:+:${PATH}}"
elif [[ "$OSTYPE" == "win32" ]]; then
os="win32"
@ -1008,8 +1164,46 @@ exitcode=0
#-- sh/bash launches nextscript here instead of shebang line at top
if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then
echo zsh/bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@"
script="$0"
if [[ "$nextshelltype" == "pwsh" || "$nextshelltype" == "powershell" ]]; then
#powershell requires the file extension to be .ps1 (always - on windows)
#on other platforms it's not required if a shebang line is used - but this script must be shebangless for portability and to maintain polyglot capabilities.
cmdpattern="[.]cmd$"
if [[ "$script" =~ $cmdpattern ]]; then
ps1script="${script%????}.ps1"
if ! cmp -s "$script" "$ps1script" ; then
#ps1script either different or missing
#on windows - batch script copies .cmd -> .ps1 if not identical
cp -f "$script" "$ps1script"
fi
script=$ps1script
fi
fi
if [[ "$plat" == "MSYS_NT"* ]]; then
#we need to deal with MSYS argument mangling
cmdpattern="^cmd.exe |^cmd "
#do not double quote cmdpattern - or it will be treated as literal string
if [[ "$nextshellpath" =~ $cmdpattern ]]; then
#for now - tell the user what's going on
echo "cmd call via msys detected. performing translation of /c to //c and escaping backslashes in script path"
#flags to cmd.exe such as /c are interpreted by msys as looking like a unix path
#review - for nextshellpath targets specified in the block for win32 - we don't expect unix paths (?)
#what about other flags? - can we just double up all forward slashes?
nextshellpath="${nextshellpath// \/c / \/\/c }"
echo "new nextshellpath: ${nextshellpath}"
#don't double quote this
script=${script//\\/\\\\}
fi
echo "calling ${nextshellpath} $script $@"
# eval is required here - but why?
eval ${nextshellpath} "$script" "$@"
else
#e.g /usr/bin/env tclsh "$0" "$@"
${nextshellpath} "$0" "$@"
${nextshellpath} "$script" "$@"
fi
exitcode=$?
#echo "zsh/bash reporting exitcode: ${exitcode}"
@ -1204,18 +1398,44 @@ if ($match.Success) {
$admininfo = $match.Groups[1].Value
$asadmin = $admininfo.Contains("asadmin=1")
if ($asadmin) {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
if ($args[0] -eq "PUNK-ELEVATED") {
# May be present if launch and elevation was done via cmd.exe script
# shift away first arg
$newargs = $args | Select-Object -Skip 1
} else {
$newargs = $args
}
# -Wait e.g for starting a service or other operations which remainder of script may depend on
$arguments = @("-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments = @("-NoProfile","-NoLogo", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments += @("-File", $($MyInvocation.MyCommand.Path))
$arguments += $args
foreach ($a in $newargs) {
if ($a -match '\s') {
$arguments += "`"$a`""
} else {
$arguments += $a
}
}
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
Write-Host "Powershell elevating using start-process with -Verb RunAs"
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -Wait -Verb RunAs
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -Wait -Verb RunAs
}
Exit # Exit the current non-elevated process
} else {
if ($args[0] -eq "PUNK-ELEVATED") {
#Already elevated (by cmd.exe)
#.. but it is impossible to modify or reassign the automatic $args variable
# so let's start yet another whole new process just to remove one leading argument so the custom script can operate on parameters cleanly - thanks powershell :/
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -NoNewWindow -Wait
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -NoNewWindow -Wait
}
Exit
}
}
}
}

538
bin/runtime.cmd

@ -95,19 +95,19 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting
@REM Supporting more explicit oses than those listed may also require script padding adjustment
: <<nextshell_start>>
@SET "nextshellpath[win32___________]=powershell -nop -nol -ExecutionPolicy bypass -File______________"
@SET "nextshellpath[win32___________]=cmd.exe /c powershell -nop -nol -ExecutionPolicy bypass -File___________________________________________________________________"
@SET "nextshelltype[win32___________]=powershell______"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[dragonflybsd____]=bash____________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[freebsd_________]=bash____________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[netbsd__________]=bash____________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[linux___________]=bash____________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[macosx__________]=bash____________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[other___________]=bash____________"
: <<nextshell_end>>
@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).
@ -119,7 +119,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed
@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed%
@SET "selected_shellpath=%nextshellpath[win32___________]%"
@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed
@CALL :stringTrimTrailingUnderscores "%selected_shellpath%" selected_shellpath_trimmed
@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%"
@REM @ECHO keyremoved %keyRemoved%
@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
@ -151,6 +151,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET "winpath=%~dp0" %= e.g c:\punkshell\bin\ %=
@SET "fname=%~nx0"
@SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =%
@SET "fullscriptname=%~dp0%~n0%~x0"
@REM @ECHO fname %fname%
@REM @ECHO winpath %winpath%
@REM @ECHO commandlineascalled %0
@ -162,6 +163,62 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringToUpper %~nx0 capscripttail
@REM @ECHO capscriptname: %capscripttail%
@goto skip_parameter_wrangling
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args and shift, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
:skip_parameter_wrangling
@IF "%nftail%"=="%capscripttail%" (
@ECHO forcing asadmin=1 due to file name on filesystem being uppercase
@SET "asadmin=1"
@ -189,31 +246,112 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
:getPrivileges
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@ECHO pre = "/c %fullscriptname% PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@REM @echo pre = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@echo args = pre >> "%vbsGetPrivileges%"
@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
@ECHO args = args ^& Chr(34) ^& strArg ^& Chr(34) ^& " " >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@GOTO skiptest
%= Option Explicit =%
%= We need a child process to locate the current script. =%
@ECHO Const FLAG_PROCESS = "winver.exe" >> "%vbsGetPrivileges%"
%= ' WMI constants %=
@ECHO Const wbemFlagForwardOnly = 32 >> "%vbsGetPrivileges%"
%=' Generate a unique value to be used as a flag =%
@ECHO Dim guid >> "%vbsGetPrivileges%
@ECHO guid = Left(CreateObject("Scriptlet.TypeLib").GUID,38) >> "%vbsGetPrivileges%"
%= ' Start a process using the indicated flag inside its command line =%
@ECHO WScript.CreateObject("WScript.Shell").Run """" ^& FLAG_PROCESS ^& """ " ^& guid, 0, False >> "%vbsGetPrivileges%"
%= ' To retrieve process information a WMI reference is needed =%
@ECHO Dim wmi >> "%vbsGetPrivileges%"
@ECHO Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}^!\\.\root\cimv2") >> "%vbsGetPrivileges%"
%= ' Query the list of processes with the flag in its command line, retrieve the =%
%= ' process ID of its parent process ( our script! ) and terminate the process =%
@ECHO Dim colProcess, process, myProcessID >> "%vbsGetPrivileges%"
@ECHO Set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT ParentProcessID From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE Name='" ^& FLAG_PROCESS ^& "' " ^& _>> "%vbsGetPrivileges%"
@ECHO "AND CommandLine LIKE '%%" ^& guid ^& "%%'" _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO myProcessID = process.ParentProcessID >> "%vbsGetPrivileges%"
@ECHO process.Terminate >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
%= ' Knowing the process id of our script we can query the process list =%
%= ' and retrieve its command line =%
@ECHO Dim commandLine >> "%vbsGetPrivileges%"
@ECHO set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT CommandLine From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE ProcessID=" ^& myProcessID _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO commandLine = process.CommandLine >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script in new window due to administrator elevation
@ECHO WScript.Echo "raw commandline: " ^& commandLine >>"%vbsGetPrivileges%"
%= ' Done =%
@ECHO intpos = 0 >> "%vbsGetPrivileges%"
@ECHO intCount = 0 >> "%vbsGetPrivileges%"
@ECHO intstartsearch = 1 >> "%vbsGetPrivileges%"
@ECHO intmax = 100 >> "%vbsGetPrivileges%"
@ECHO do While intCount ^< 4 and intmax ^> 0 >> "%vbsGetPrivileges%"
@ECHO intpos = InStr(intstartsearch, commandline, """") >> "%vbsGetPrivileges%"
@ECHO if intpos ^<^> 0 then >> "%vbsGetPrivileges%"
@ECHO intCount = intCount + 1 >> "%vbsGetPrivileges%"
@ECHO if intcount = 4 then >> "%vbsGetPrivileges%"
@ECHO ' wscript.echo "position: " ^& intpos >> "%vbsGetPrivileges%"
@ECHO commandline = Mid(commandline,intpos+1) >> "%vbsGetPrivileges%"
@ECHO exit do >> "%vbsGetPrivileges%"
@ECHO else >> "%vbsGetPrivileges%"
@ECHO intstartsearch = intpos + 1 >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO intmax = intmax -1 >> "%vbsGetPrivileges%"
@ECHO Loop >> "%vbsGetPrivileges%"
@ECHO if intcount ^< 4 then >> "%vbsGetPrivileges%"
@ECHO err.raise vbObjectError + 1001, "vbsGetPrivileges", "failed to parse commandline" >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO commandline = pre ^& commandline >> "%vbsGetPrivileges%"
@ECHO WScript.Echo "commandline: " ^& commandLine >>"%vbsGetPrivileges%"
@ECHO WScript.Echo "args: " ^& args >>"%vbsGetPrivileges%"
:skiptest
@ECHO UAC.ShellExecute "cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@REM @ECHO UAC.ShellExecute "%fullscriptname%", commandline, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script "%fullscriptname%" in new window due to administrator elevation with args: "%*"
@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
@REM @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" !newcommandline!
@EXIT /B
@REM buffer
@REM buffer
:gotPrivileges
@REM setlocal & pushd .
@PUSHD .
@cd /d %~dp0
@cd /d %winpath%
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
@DEL "%vbsGetPrivileges%" 1>nul 2>nul
@SET arglist=%arglist:~14%
@SHIFT
)
:skip_privileges
@SET need_ps1=0
@REM we want the ps1 to exist even if the nextshell isn't powershell
@if not exist "%~dp0%~n0.ps1" (
@if not exist "%scriptrootname%.ps1" (
@SET need_ps1=1
) ELSE (
fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different
fc "%fullscriptname%" "%scriptrootname%.ps1" >nul || goto different
@REM @ECHO "files same"
@SET need_ps1=0
)
@ -223,74 +361,13 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET need_ps1=1
:pscontinue
@IF !need_ps1!==1 (
COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL
COPY "%fullscriptname%" "%scriptrootname%.ps1" >NUL
)
@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
@IF "!selected_shelltype_trimmed!"=="none" (
SET selected_shelltype_trimmed=pwsh
)
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
@REM @SET "squoted_args="
@REM @for %%a in (%*) do @(
@REM set "v=%%a"
@ -311,19 +388,24 @@ SETLOCAL EnableDelayedExpansion
REM fallback to powershell if pwsh failed
IF !pwshtest_exitcode!==0 (
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist%
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted
cmd /c pwsh -nop -nol -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
@rem pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" %arglist% %= ok =%
@rem cmd /c pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
SET task_exitcode=!errorlevel!
) ELSE (
REM TODO prompt user with option to call script to install pwsh using winget
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
)
) ELSE (
IF "!selected_shelltype_trimmed!"=="powershell" (
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
@rem @echo powershell - !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist% %= ok - this works =%
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
) ELSE (
IF "!selected_shelltype_trimmed!"=="wslbash" (
@ -337,24 +419,23 @@ SETLOCAL EnableDelayedExpansion
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 The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode
@ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
%selected_shellpath_trimmed% "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
@REM @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
!selected_shellpath_trimmed! "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
) ELSE (
ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes%
SET task_exitcode=66
@REM boundary padding
GOTO :exit_multishell
)
)
)
)
@REM batch file library functions
@GOTO :endlib
@REM padding
@REM padding
@REM padding
@REM padding
%= ---------------------------------------------------------------------- =%
@rem courtesy of dbenham
@ -458,6 +539,7 @@ do if not defined param1 set %%~"param1=%2%%~"
rem %1 #%2#
@exit /b
@rem padding
@REM courtesy of: https://stackoverflow.com/users/463115/jeb
:strlen stringVar returnVar
@(
@ -506,6 +588,8 @@ do if not defined param1 set %%~"param1=%2%%~"
)
@EXIT /B
@REM padding
@REM padding
: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
@ -545,6 +629,7 @@ do if not defined param1 set %%~"param1=%2%%~"
@EXIT /B
@REM boundary padding
@REM boundary padding
@REM boundary padding
:getNormalizedScriptTail
@SETLOCAL
@SET "result=%~nx0"
@ -654,14 +739,14 @@ do if not defined param1 set %%~"param1=%2%%~"
@REM boundary padding
@REM boundary padding
@REM boundary padding
@REM boundary padding
:stringTrimTrailingUnderscores
@SETLOCAL
@SET "rtrn=%~2"
@SET "string=%~1"
@SET "trimstring=%~1"
@REM trim up to 63 underscores from the end of a string using string substitution
@REM trim up to 127 underscores from the end of a string using string substitution
@SET "trimstring=%trimstring%###"
@SET "trimstring=%trimstring:________________________________________________________________###=###%"
@SET "trimstring=%trimstring:________________________________###=###%"
@SET "trimstring=%trimstring:________________###=###%"
@SET "trimstring=%trimstring:________###=###%"
@ -707,10 +792,11 @@ do if not defined param1 set %%~"param1=%2%%~"
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- 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
# -- It is tuned to run (and possibly divert to different payload shell) when called from cmd.exe as a batch file, tclsh,sh,zsh,bash,perl or pwsh/powershell script
# -- i.e it is a polyglot file.
# -- The payload target (by os) is defined in the nextshell block at the top which is constructed when generating the polyglot
# -- using the tcl 'dev scriptwrap.multishell' command in a tcl punk shell
# -- The payload can be tcl,perl,powershell/pwsh or zsh/bash.
# -- 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 ./scriptname.cmd in sh or zsh or bash
@ -720,7 +806,13 @@ do if not defined param1 set %%~"param1=%2%%~"
rename set ""; rename S set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
Hide :exit_multishell;Hide {<#};Hide '@
#---------------------------------------------------------------------
puts "info script : [info script]"
#puts "argcount : $::argc"
#puts "argvalues: $::argv"
#puts "argv0 : $::argv0"
# -- --- --- --- --- --- --- --- --- --- --- ---
#divert to configured nextshell
set script_as_called [info script]
package require platform
set plat_full [platform::generic]
set plat [lindex [split $plat_full -] 0]
@ -733,6 +825,14 @@ set in_data 0
set nextshellpath ""
set nextshelltype ""
puts stderr "PLAT: $plat"
switch -glob -- $plat {
"msys" - "mingw*" {
set os "win32"
}
default {
set os $plat
}
}
foreach ln [split $scriptdata \n] {
if {[string trim $ln] eq ""} {continue}
if {!$in_data} {
@ -740,14 +840,14 @@ foreach ln [split $scriptdata \n] {
set in_data 1
}
} else {
if {[string match "*@SET*nextshellpath?${plat}_*" $ln]} {
if {[string match "*@SET*nextshellpath?${os}_*" $ln]} {
set lineparts [split $ln =]
set tail [lindex $lineparts 1]
set nextshellpath [string trimright $tail {_"}]
if {$nextshellpath ne "" && $nextshelltype ne ""} {
break
}
} elseif {[string match "*@SET*nextshelltype?${plat}_*" $ln]} {
} elseif {[string match "*@SET*nextshelltype?${os}_*" $ln]} {
set lineparts [split $ln =]
set tail [lindex $lineparts 1]
set nextshelltype [string trimright $tail {_"}]
@ -760,31 +860,111 @@ foreach ln [split $scriptdata \n] {
}
}
if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} {
set script_rootname [file rootname $script_as_called]
if {$nextshelltype in "pwsh powershell"} {
set scrname [file rootname [info script]].ps1
set arglist [list]
foreach a $::argv {
set a "'$a'"
lappend arglist $a
# experimental
set script_ps1 $script_rootname.ps1
set arglist $::argv
if {[file extension $script_as_called] ne ".ps1"} {
#we need to ensure .ps1 is up to date
set needs_updating 0
if {![file exists $script_ps1]} {
set needs_updating 1
} else {
#both exist
if {[file size $script_as_called] != [file size $script_ps1]} {
set needs_updating 1
} else {
#both exist with same size - do full check that they're identical
catch {package require sha256}
if {[package provide sha256] ne ""} {
set h1 [sha2::sha256 -hex -file $script_as_called]
set h2 [sha2::sha256 -hex -file $script_ps1]
if {[string length $h1] != 64 || [string length $h2] != 64} {
set needs_updating 1
} elseif {$h1 ne $h2} {
set needs_updating 1
}
} else {
#manually compare - scripts aren't too big, so slurp and string compare is fine
set fd [open $script_as_called]
chan configure $fd -translation binary
set data1 [read $fd]
close $fd
set fd [open $script_ps1]
chan configure $fd -translation binary
set data2 [read $fd]
close $fd
if {![string equal $data1 $data2]} {
set needs_updating 1
}
}
}
}
if {$needs_updating} {
file copy -force $script_as_called $script_ps1
}
} else {
set scrname [info script]
#when called on the .ps1 - we assume it's up to date - review
}
set scrname $script_ps1
#set arglist [list]
#foreach a $::argv {
# set a "'$a'"
# lappend arglist $a
#}
} else {
set scrname $script_as_called
set arglist $::argv
}
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist"
#todo - handle /usr/bin/env
#todo - exitcode
if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} {
set nextshell_words [list $nextshellpath]
#review - test spaced quoted words in nextshellpath?
#
#if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} {
# set nextshell_words [list $nextshellpath]
#} else {
# set nextshell_words $nextshellpath
#}
#perform any msys argument munging on a cmd/cmd.exe based nextshellpath before we convert the first word to an auto_exec path
switch -glob -- $plat {
"msys" - "mingw*" {
set cmdword [lindex $nextshellpath 0]
#we only act on cmd or cmd.exe - not a full path such as c:/WINDOWS/system32/cmd.exe
#the nextshellpath should generally be configured as cmd /c ... or cmd.exe ... but specifying it as a path could allow bypassing this un-munging.
#The un-munging only applies to msys/mingw, so such bypassing should be unnecessary - review
#maint: keep this munging in sync with zsh/bash and perl blocks which must also do msys mangling
if {[regexp {^cmd$|^cmd[.]exe$} $cmdword]} {
#need to deal with msys argument munging
#for now we only deal with /C or /c - todo - other cmd.exe flags?
#In this context we would usually only be using cmd.exe /c to launch older 'desktop' powershell to avoid spaced-argument problems - so we aren't expecting other flags
set new_nextshellpath [list $cmdword]
#for now - just do what zsh munging does - bash regex/string/array processing is tedious and footgunny for the unfamiliar (me),
#so determine the minimum viable case for code there, then port behaviour to perl/tcl msys munging sections.
foreach w [lrange $nextshellpath 1 end] {
if {[regexp {^/[Cc]$} $w]} {
lappend new_nextshellpath {//C}
} else {
set nextshell_words $nextshellpath
lappend new_nextshellpath $w
}
}
set nextshellpath $new_nextshellpath
}
set ns_firstword [lindex $nextshellpath 0]
if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} {
set ns_firstword [string range $ns_firstword 1 end-1]
}
}
set ns_firstword [lindex $nextshellpath 0]
#review - is this test for extra layer of double quoting on first word really necessary?
#if we are treaing $nextshellpath as a tcl list - the first layer of double quotes will already have disappeared
##if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} {
## set ns_firstword [string range $ns_firstword 1 end-1]
##}
if {[string match {/*/env} $ns_firstword] && $::tcl_platform(platform) ne "windows"} {
if {$::tcl_platform(platform) ne "windows" && [string match {/*/env} $ns_firstword]} {
set exec_part $nextshellpath
} else {
set epath [auto_execok $ns_firstword]
@ -794,6 +974,10 @@ if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} {
set exec_part [list {*}$epath {*}[lrange $nextshellpath 1 end]]
}
}
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist"
puts stdout "exec: $exec_part $scrname $arglist"
catch {exec {*}$exec_part $scrname {*}$arglist <@stdin >@stdout 2>@stderr} emsg eopts
if {[dict exists $eopts -errorcode]} {
@ -837,11 +1021,6 @@ namespace eval ::punk::multishell {
}
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
#puts "script : [info script]"
#puts "argcount : $::argc"
#puts "argvalues: $::argv"
#puts "argv0 : $::argv0"
# -- --- --- --- --- --- --- --- --- --- --- ---
#<tcl-payload>
puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl"
@ -867,8 +1046,11 @@ if {[::punk::multishell::is_main]} {
HEREDOC1B_HIDE_FROM_BASH_AND_SH
# Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \
echo "var0: $0 @: $@"
# use oldschool backticks and sed - lowest common denominator \
ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
# use oldschool backticks and sed (posix - lowest common denominator) \
# ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` \
# some ps impls will return arguments - so last field not always appropriate \
# some ps impls don't have -o (e.g cygwin) so ps_shellname may remain empty and emit an error \
ps_shellname=`ps -o pid,comm -p $$ | awk '$1 != "PID" {print $2}'`
# \
echo "shell from ps: $ps_shellname"
# \
@ -900,8 +1082,11 @@ pop() {
}
# ------------------------------------------------------------------------------
# non-bash-like posix diversion \
if [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ]; then
# non-bash-like posix diversion
# we don't use $BASH_VERSION/$ZSH_VERSION as these can still be set when for example
# sh is a symlink to bash (posix-mode bash - reduced bashism capabilities?)
# if our ps_shellname didn't contain a result, don't divert and risk looping
if [ -n "$ps_shellname" ] && [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ] ; then
shift
pop $#
eval "$POP_EXPR"
@ -919,17 +1104,14 @@ if false==false # else {
then
: #
# zsh/bash \
shift && set -- "${@:1:$((${#@}-1))}"
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- 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 the %nextshell% value above
# -- if sh/bash scripting needs to run on windows too.
# -- zsh/bash script section
# --
# -- review - for zsh do we want to use: setopt KSH_ARRAYS ?
# -- arrays in bash 0-based vs 1-based in zsh
# -- stick to the @:i:len syntax which is same for both
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
plat=$(uname -s) #platform/system
@ -953,17 +1135,30 @@ elif [[ "$plat" == "CYGWIN_NT"* ]]; then
os="win32"
elif [[ "$plat" == "MSYS_NT"* ]]; then
#review..
echo MSYS
#win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#Need to consider the difference between when msys2 was launched (which strips some paths and sets up the environment)
# vs if the msys2 sh was called - (actually bash) in which case paths will be different
#wsl and cygwin or msys2 can commonly be problematic combinations - primarily due to path issues
#e.g "c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#e.g It means a /usr/bin/env bash call may launch the (linux elf) bash for wsl rather than the msys bash
#
#msys provides win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#bash reports $OSTYPE msys
#there are statements around the web that cmd /c .. will work under msys2
# - but from experience, it can be required to use cmd //c ...
# or MSYS2_ARG_CONV_ECL='*' cmd /c ..
# This seems to be because process arguments that look like unix paths are converted to windows paths :/
#review!
os="win32"
#review - need ps/sed/awk to determine shell?
interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
interp=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
#use 'command -v' (shell builtin preferred over external which)
shellpath=`command -v $interp`
shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname
#"c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#This breaks calls to various unix utils such as sed etc (wsl related?)
export PATH="$shellfolder${PATH:+:${PATH}}"
elif [[ "$OSTYPE" == "win32" ]]; then
os="win32"
@ -985,6 +1180,8 @@ elif [[ "$ps_shellname" == "zsh" ]]; then
else
#fallback - doesn't seem to work in zsh - untested in early bash
IFS=$'\n' arr_oslines=($shellconfiglines)
IFS=$' \t\n'
# review
fi
nextshellpath=""
nextshelltype=""
@ -1008,8 +1205,55 @@ exitcode=0
#-- sh/bash launches nextscript here instead of shebang line at top
if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then
echo zsh/bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@"
script="$0"
if [[ "$nextshelltype" == "pwsh" || "$nextshelltype" == "powershell" ]]; then
#powershell requires the file extension to be .ps1 (always - on windows)
#on other platforms it's not required if a shebang line is used - but this script must be shebangless for portability and to maintain polyglot capabilities.
cmdpattern="[.]cmd$"
if [[ "$script" =~ $cmdpattern ]]; then
ps1script="${script%????}.ps1"
if ! cmp -s "$script" "$ps1script" ; then
#ps1script either different or missing
#on windows - batch script copies .cmd -> .ps1 if not identical
cp -f "$script" "$ps1script"
fi
script=$ps1script
fi
fi
if [[ "$plat" == "MSYS_NT"* ]]; then
#we need to deal with MSYS argument munging
cmdpattern="^cmd.exe |^cmd "
#do not double quote cmdpattern - or it will be treated as literal string
if [[ "$nextshellpath" =~ $cmdpattern ]]; then
#for now - tell the user what's going on
echo "cmd call via msys detected. performing translation of /c to //c and escaping backslashes in script path"
#flags to cmd.exe such as /c are interpreted by msys as looking like a unix path
#review - for nextshellpath targets specified in the block for win32 - we don't expect unix paths (?)
#what about other flags? - can we just double up all forward slashes?
#maint: keep this munging in sync with the tcl block and perl block which must also do msys munging
nextshellpath="${nextshellpath// \/[cC] / \/\/c }"
# echo "new nextshellpath: ${nextshellpath}"
#don't double quote this
script=${script//\\/\\\\}
fi
echo "calling ${nextshellpath} $script $@"
#load into array
cmd_array=($nextshellpath)
cmd_array+=("$script") #add script, which may contain spaces as a single entry ?
cmd_array+=( "$@" ) #add each element of args to array as a separate entry (equiv ? "${arr[@]}")
# printf "%s\n" "${cmd_array[@]}"
"${cmd_array[@]}"
# this works to make nextshellpath run - but joins $@ members incorrectly
#eval ${nextshellpath} "$script" "$@"
else
#e.g /usr/bin/env tclsh "$0" "$@"
${nextshellpath} "$0" "$@"
${nextshellpath} "$script" "$@"
fi
exitcode=$?
#echo "zsh/bash reporting exitcode: ${exitcode}"
@ -1328,18 +1572,44 @@ if ($match.Success) {
$admininfo = $match.Groups[1].Value
$asadmin = $admininfo.Contains("asadmin=1")
if ($asadmin) {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
if ($args[0] -eq "PUNK-ELEVATED") {
# May be present if launch and elevation was done via cmd.exe script
# shift away first arg
$newargs = $args | Select-Object -Skip 1
} else {
$newargs = $args
}
# -Wait e.g for starting a service or other operations which remainder of script may depend on
$arguments = @("-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments = @("-NoProfile","-NoLogo", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments += @("-File", $($MyInvocation.MyCommand.Path))
$arguments += $args
foreach ($a in $newargs) {
if ($a -match '\s') {
$arguments += "`"$a`""
} else {
$arguments += $a
}
}
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
Write-Host "Powershell elevating using start-process with -Verb RunAs"
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -Wait -Verb RunAs
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -Wait -Verb RunAs
}
Exit # Exit the current non-elevated process
} else {
if ($args[0] -eq "PUNK-ELEVATED") {
#Already elevated (by cmd.exe)
#.. but it is impossible to modify or reassign the automatic $args variable
# so let's start yet another whole new process just to remove one leading argument so the custom script can operate on parameters cleanly - thanks powershell :/
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -NoNewWindow -Wait
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -NoNewWindow -Wait
}
Exit
}
}
}
}

686
bin/tclargs.cmd

File diff suppressed because it is too large Load Diff

541
getpunk.cmd

@ -95,19 +95,19 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting
@REM Supporting more explicit oses than those listed may also require script padding adjustment
: <<nextshell_start>>
@SET "nextshellpath[win32___________]=powershell -nop -nol -ExecutionPolicy ByPass -File______________"
@SET "nextshellpath[win32___________]=cmd.exe /c powershell -nop -nol -ExecutionPolicy ByPass -File___________________________________________________________________"
@SET "nextshelltype[win32___________]=powershell______"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[dragonflybsd____]=bash____________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[freebsd_________]=bash____________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[netbsd__________]=bash____________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[linux___________]=bash____________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[macosx__________]=bash____________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________"
@SET "nextshellpath[other___________]=/usr/bin/env bash_______________________________________________________________________________________________________________"
@SET "nextshelltype[other___________]=bash____________"
: <<nextshell_end>>
@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).
@ -119,7 +119,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed
@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed%
@SET "selected_shellpath=%nextshellpath[win32___________]%"
@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed
@CALL :stringTrimTrailingUnderscores "%selected_shellpath%" selected_shellpath_trimmed
@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%"
@REM @ECHO keyremoved %keyRemoved%
@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available
@ -151,6 +151,7 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET "winpath=%~dp0" %= e.g c:\punkshell\bin\ %=
@SET "fname=%~nx0"
@SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =%
@SET "fullscriptname=%~dp0%~n0%~x0"
@REM @ECHO fname %fname%
@REM @ECHO winpath %winpath%
@REM @ECHO commandlineascalled %0
@ -162,6 +163,62 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@CALL :stringToUpper %~nx0 capscripttail
@REM @ECHO capscriptname: %capscripttail%
@goto skip_parameter_wrangling
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args and shift, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
:skip_parameter_wrangling
@IF "%nftail%"=="%capscripttail%" (
@ECHO forcing asadmin=1 due to file name on filesystem being uppercase
@SET "asadmin=1"
@ -189,31 +246,112 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
:getPrivileges
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges )
@ECHO Set UAC = CreateObject^("Shell.Application"^) > "%vbsGetPrivileges%"
@ECHO args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@ECHO pre = "/c %fullscriptname% PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@REM @echo pre = "PUNK-ELEVATED " >> "%vbsGetPrivileges%"
@echo args = pre >> "%vbsGetPrivileges%"
@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%"
@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%"
@ECHO args = args ^& Chr(34) ^& strArg ^& Chr(34) ^& " " >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@GOTO skiptest
%= Option Explicit =%
%= We need a child process to locate the current script. =%
@ECHO Const FLAG_PROCESS = "winver.exe" >> "%vbsGetPrivileges%"
%= ' WMI constants %=
@ECHO Const wbemFlagForwardOnly = 32 >> "%vbsGetPrivileges%"
%=' Generate a unique value to be used as a flag =%
@ECHO Dim guid >> "%vbsGetPrivileges%
@ECHO guid = Left(CreateObject("Scriptlet.TypeLib").GUID,38) >> "%vbsGetPrivileges%"
%= ' Start a process using the indicated flag inside its command line =%
@ECHO WScript.CreateObject("WScript.Shell").Run """" ^& FLAG_PROCESS ^& """ " ^& guid, 0, False >> "%vbsGetPrivileges%"
%= ' To retrieve process information a WMI reference is needed =%
@ECHO Dim wmi >> "%vbsGetPrivileges%"
@ECHO Set wmi = GetObject("winmgmts:{impersonationLevel=impersonate}^!\\.\root\cimv2") >> "%vbsGetPrivileges%"
%= ' Query the list of processes with the flag in its command line, retrieve the =%
%= ' process ID of its parent process ( our script! ) and terminate the process =%
@ECHO Dim colProcess, process, myProcessID >> "%vbsGetPrivileges%"
@ECHO Set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT ParentProcessID From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE Name='" ^& FLAG_PROCESS ^& "' " ^& _>> "%vbsGetPrivileges%"
@ECHO "AND CommandLine LIKE '%%" ^& guid ^& "%%'" _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO myProcessID = process.ParentProcessID >> "%vbsGetPrivileges%"
@ECHO process.Terminate >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script in new window due to administrator elevation
%= ' Knowing the process id of our script we can query the process list =%
%= ' and retrieve its command line =%
@ECHO Dim commandLine >> "%vbsGetPrivileges%"
@ECHO set colProcess = wmi.ExecQuery( _>> "%vbsGetPrivileges%"
@ECHO "SELECT CommandLine From Win32_Process " ^& _>> "%vbsGetPrivileges%"
@ECHO "WHERE ProcessID=" ^& myProcessID _>> "%vbsGetPrivileges%"
@ECHO ,"WQL" , wbemFlagForwardOnly _>> "%vbsGetPrivileges%"
@ECHO ) >> "%vbsGetPrivileges%"
@ECHO For Each process In colProcess >> "%vbsGetPrivileges%"
@ECHO commandLine = process.CommandLine >> "%vbsGetPrivileges%"
@ECHO Next >> "%vbsGetPrivileges%"
@ECHO WScript.Echo "raw commandline: " ^& commandLine >>"%vbsGetPrivileges%"
%= ' Done =%
@ECHO intpos = 0 >> "%vbsGetPrivileges%"
@ECHO intCount = 0 >> "%vbsGetPrivileges%"
@ECHO intstartsearch = 1 >> "%vbsGetPrivileges%"
@ECHO intmax = 100 >> "%vbsGetPrivileges%"
@ECHO do While intCount ^< 4 and intmax ^> 0 >> "%vbsGetPrivileges%"
@ECHO intpos = InStr(intstartsearch, commandline, """") >> "%vbsGetPrivileges%"
@ECHO if intpos ^<^> 0 then >> "%vbsGetPrivileges%"
@ECHO intCount = intCount + 1 >> "%vbsGetPrivileges%"
@ECHO if intcount = 4 then >> "%vbsGetPrivileges%"
@ECHO ' wscript.echo "position: " ^& intpos >> "%vbsGetPrivileges%"
@ECHO commandline = Mid(commandline,intpos+1) >> "%vbsGetPrivileges%"
@ECHO exit do >> "%vbsGetPrivileges%"
@ECHO else >> "%vbsGetPrivileges%"
@ECHO intstartsearch = intpos + 1 >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO intmax = intmax -1 >> "%vbsGetPrivileges%"
@ECHO Loop >> "%vbsGetPrivileges%"
@ECHO if intcount ^< 4 then >> "%vbsGetPrivileges%"
@ECHO err.raise vbObjectError + 1001, "vbsGetPrivileges", "failed to parse commandline" >> "%vbsGetPrivileges%"
@ECHO end if >> "%vbsGetPrivileges%"
@ECHO commandline = pre ^& commandline >> "%vbsGetPrivileges%"
@ECHO WScript.Echo "commandline: " ^& commandLine >>"%vbsGetPrivileges%"
@ECHO WScript.Echo "args: " ^& args >>"%vbsGetPrivileges%"
:skiptest
@ECHO UAC.ShellExecute "cmd.exe", args, "", "runas", 1 >> "%vbsGetPrivileges%"
@REM @ECHO UAC.ShellExecute "%fullscriptname%", commandline, "", "runas", 1 >> "%vbsGetPrivileges%"
@ECHO Launching script "%fullscriptname%" in new window due to administrator elevation with args: "%*"
@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %*
@REM @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" !newcommandline!
@EXIT /B
@REM buffer
@REM buffer
:gotPrivileges
@REM setlocal & pushd .
@PUSHD .
@cd /d %~dp0
@cd /d %winpath%
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (
@DEL "%vbsGetPrivileges%" 1>nul 2>nul
@SET arglist=%arglist:~14%
@SHIFT
)
:skip_privileges
@SET need_ps1=0
@REM we want the ps1 to exist even if the nextshell isn't powershell
@if not exist "%~dp0%~n0.ps1" (
@if not exist "%scriptrootname%.ps1" (
@SET need_ps1=1
) ELSE (
fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different
fc "%fullscriptname%" "%scriptrootname%.ps1" >nul || goto different
@REM @ECHO "files same"
@SET need_ps1=0
)
@ -223,74 +361,13 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n%
@SET need_ps1=1
:pscontinue
@IF !need_ps1!==1 (
COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL
COPY "%fullscriptname%" "%scriptrootname%.ps1" >NUL
)
@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /?
@IF "!selected_shelltype_trimmed!"=="none" (
SET selected_shelltype_trimmed=pwsh
)
@set argCount=30
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead.
@set tmpfile_base=%TEMP%\punkbatch_params
@call :getUniqueFile %tmpfile_base% ".txt" paramfile
@echo %paramfile%
%= NOTE when we loop like this using the percent-n args, we lose unquoted separators such as comma and semicolon %=
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124
@rem outer loop required to redirect all rem lines at once to file
@for %%x in (1) do @(
@for /L %%f in (1,1,%argCount%) do @(
@set "argnum=%%~nf"
@set "a1=%%1"
@rem @set "argname=%%!argnum!"
@rem @echo argname: !argname!
@call :rem_output !argnum! !a1!
@shift
)
) > %paramfile%
@echo off
@set "newcommandline= "
@(set target=cmd_pwsh)
@if "%target%"=="cmd_pwsh" (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
@REM @echo ######### %%L
@rem call :buildcmdline newcommandline param "{" "}"
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %=
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %=
@rem @echo .
)
) ELSE (
@for /F "delims=" %%L in (%paramfile%) do @(
SETLOCAL DisableDelayedExpansion
set "param=%%L"
call :buildcmdline newcommandline param
)
)
@REM padding
SETLOCAL EnableDelayedExpansion
@echo off
@IF EXIST %paramfile% (
@DEL /F /Q %paramfile%
)
@IF EXIST %paramfile% (
echo failed to delete %paramfile%
cat %paramfile%
)
@REM @SET "squoted_args="
@REM @for %%a in (%*) do @(
@REM set "v=%%a"
@ -311,19 +388,24 @@ SETLOCAL EnableDelayedExpansion
REM fallback to powershell if pwsh failed
IF !pwshtest_exitcode!==0 (
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist%
@rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted
cmd /c pwsh -nop -nol -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
@rem pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" %arglist% %= ok =%
@rem cmd /c pwsh -nop -nologo -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline!
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
SET task_exitcode=!errorlevel!
) ELSE (
REM TODO prompt user with option to call script to install pwsh using winget
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
)
) ELSE (
IF "!selected_shelltype_trimmed!"=="powershell" (
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist%
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
%= powershell with -file flag treats it's arguments differently to pwsh - we need cmd /c to preserve args with spaces =%
@rem @echo powershell - !selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" %arglist% %= ok - this works =%
!selected_shellpath_trimmed! "%scriptrootname%.ps1" %arglist%
@rem cmd /c powershell -nop -nologo -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline!
SET task_exitcode=!errorlevel!
) ELSE (
IF "!selected_shelltype_trimmed!"=="wslbash" (
@ -337,24 +419,23 @@ SETLOCAL EnableDelayedExpansion
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 The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode
@ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
%selected_shellpath_trimmed% "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
@REM @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!"
!selected_shellpath_trimmed! "%winpath%%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call;
) ELSE (
ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes%
SET task_exitcode=66
@REM boundary padding
GOTO :exit_multishell
)
)
)
)
@REM batch file library functions
@GOTO :endlib
@REM padding
@REM padding
@REM padding
@REM padding
%= ---------------------------------------------------------------------- =%
@rem courtesy of dbenham
@ -458,6 +539,7 @@ do if not defined param1 set %%~"param1=%2%%~"
rem %1 #%2#
@exit /b
@rem padding
@REM courtesy of: https://stackoverflow.com/users/463115/jeb
:strlen stringVar returnVar
@(
@ -506,6 +588,8 @@ do if not defined param1 set %%~"param1=%2%%~"
)
@EXIT /B
@REM padding
@REM padding
: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
@ -545,6 +629,7 @@ do if not defined param1 set %%~"param1=%2%%~"
@EXIT /B
@REM boundary padding
@REM boundary padding
@REM boundary padding
:getNormalizedScriptTail
@SETLOCAL
@SET "result=%~nx0"
@ -654,14 +739,14 @@ do if not defined param1 set %%~"param1=%2%%~"
@REM boundary padding
@REM boundary padding
@REM boundary padding
@REM boundary padding
:stringTrimTrailingUnderscores
@SETLOCAL
@SET "rtrn=%~2"
@SET "string=%~1"
@SET "trimstring=%~1"
@REM trim up to 63 underscores from the end of a string using string substitution
@REM trim up to 127 underscores from the end of a string using string substitution
@SET "trimstring=%trimstring%###"
@SET "trimstring=%trimstring:________________________________________________________________###=###%"
@SET "trimstring=%trimstring:________________________________###=###%"
@SET "trimstring=%trimstring:________________###=###%"
@SET "trimstring=%trimstring:________###=###%"
@ -707,10 +792,11 @@ do if not defined param1 set %%~"param1=%2%%~"
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- 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
# -- It is tuned to run (and possibly divert to different payload shell) when called from cmd.exe as a batch file, tclsh,sh,zsh,bash,perl or pwsh/powershell script
# -- i.e it is a polyglot file.
# -- The payload target (by os) is defined in the nextshell block at the top which is constructed when generating the polyglot
# -- using the tcl 'dev scriptwrap.multishell' command in a tcl punk shell
# -- The payload can be tcl,perl,powershell/pwsh or zsh/bash.
# -- 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 ./scriptname.cmd in sh or zsh or bash
@ -720,7 +806,13 @@ do if not defined param1 set %%~"param1=%2%%~"
rename set ""; rename S set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore
Hide :exit_multishell;Hide {<#};Hide '@
#---------------------------------------------------------------------
puts "info script : [info script]"
#puts "argcount : $::argc"
#puts "argvalues: $::argv"
#puts "argv0 : $::argv0"
# -- --- --- --- --- --- --- --- --- --- --- ---
#divert to configured nextshell
set script_as_called [info script]
package require platform
set plat_full [platform::generic]
set plat [lindex [split $plat_full -] 0]
@ -733,6 +825,14 @@ set in_data 0
set nextshellpath ""
set nextshelltype ""
puts stderr "PLAT: $plat"
switch -glob -- $plat {
"msys" - "mingw*" {
set os "win32"
}
default {
set os $plat
}
}
foreach ln [split $scriptdata \n] {
if {[string trim $ln] eq ""} {continue}
if {!$in_data} {
@ -740,14 +840,14 @@ foreach ln [split $scriptdata \n] {
set in_data 1
}
} else {
if {[string match "*@SET*nextshellpath?${plat}_*" $ln]} {
if {[string match "*@SET*nextshellpath?${os}_*" $ln]} {
set lineparts [split $ln =]
set tail [lindex $lineparts 1]
set nextshellpath [string trimright $tail {_"}]
if {$nextshellpath ne "" && $nextshelltype ne ""} {
break
}
} elseif {[string match "*@SET*nextshelltype?${plat}_*" $ln]} {
} elseif {[string match "*@SET*nextshelltype?${os}_*" $ln]} {
set lineparts [split $ln =]
set tail [lindex $lineparts 1]
set nextshelltype [string trimright $tail {_"}]
@ -760,31 +860,111 @@ foreach ln [split $scriptdata \n] {
}
}
if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} {
set script_rootname [file rootname $script_as_called]
if {$nextshelltype in "pwsh powershell"} {
set scrname [file rootname [info script]].ps1
set arglist [list]
foreach a $::argv {
set a "'$a'"
lappend arglist $a
# experimental
set script_ps1 $script_rootname.ps1
set arglist $::argv
if {[file extension $script_as_called] ne ".ps1"} {
#we need to ensure .ps1 is up to date
set needs_updating 0
if {![file exists $script_ps1]} {
set needs_updating 1
} else {
#both exist
if {[file size $script_as_called] != [file size $script_ps1]} {
set needs_updating 1
} else {
#both exist with same size - do full check that they're identical
catch {package require sha256}
if {[package provide sha256] ne ""} {
set h1 [sha2::sha256 -hex -file $script_as_called]
set h2 [sha2::sha256 -hex -file $script_ps1]
if {[string length $h1] != 64 || [string length $h2] != 64} {
set needs_updating 1
} elseif {$h1 ne $h2} {
set needs_updating 1
}
} else {
#manually compare - scripts aren't too big, so slurp and string compare is fine
set fd [open $script_as_called]
chan configure $fd -translation binary
set data1 [read $fd]
close $fd
set fd [open $script_ps1]
chan configure $fd -translation binary
set data2 [read $fd]
close $fd
if {![string equal $data1 $data2]} {
set needs_updating 1
}
}
}
}
if {$needs_updating} {
file copy -force $script_as_called $script_ps1
}
} else {
set scrname [info script]
#when called on the .ps1 - we assume it's up to date - review
}
set scrname $script_ps1
#set arglist [list]
#foreach a $::argv {
# set a "'$a'"
# lappend arglist $a
#}
} else {
set scrname $script_as_called
set arglist $::argv
}
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist"
#todo - handle /usr/bin/env
#todo - exitcode
if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} {
set nextshell_words [list $nextshellpath]
#review - test spaced quoted words in nextshellpath?
#
#if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} {
# set nextshell_words [list $nextshellpath]
#} else {
# set nextshell_words $nextshellpath
#}
#perform any msys argument munging on a cmd/cmd.exe based nextshellpath before we convert the first word to an auto_exec path
switch -glob -- $plat {
"msys" - "mingw*" {
set cmdword [lindex $nextshellpath 0]
#we only act on cmd or cmd.exe - not a full path such as c:/WINDOWS/system32/cmd.exe
#the nextshellpath should generally be configured as cmd /c ... or cmd.exe ... but specifying it as a path could allow bypassing this un-munging.
#The un-munging only applies to msys/mingw, so such bypassing should be unnecessary - review
#maint: keep this munging in sync with zsh/bash and perl blocks which must also do msys mangling
if {[regexp {^cmd$|^cmd[.]exe$} $cmdword]} {
#need to deal with msys argument munging
#for now we only deal with /C or /c - todo - other cmd.exe flags?
#In this context we would usually only be using cmd.exe /c to launch older 'desktop' powershell to avoid spaced-argument problems - so we aren't expecting other flags
set new_nextshellpath [list $cmdword]
#for now - just do what zsh munging does - bash regex/string/array processing is tedious and footgunny for the unfamiliar (me),
#so determine the minimum viable case for code there, then port behaviour to perl/tcl msys munging sections.
foreach w [lrange $nextshellpath 1 end] {
if {[regexp {^/[Cc]$} $w]} {
lappend new_nextshellpath {//C}
} else {
set nextshell_words $nextshellpath
lappend new_nextshellpath $w
}
}
set nextshellpath $new_nextshellpath
}
}
set ns_firstword [lindex $nextshellpath 0]
if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} {
set ns_firstword [string range $ns_firstword 1 end-1]
}
if {[string match {/*/env} $ns_firstword] && $::tcl_platform(platform) ne "windows"} {
set ns_firstword [lindex $nextshellpath 0]
#review - is this test for extra layer of double quoting on first word really necessary?
#if we are treaing $nextshellpath as a tcl list - the first layer of double quotes will already have disappeared
##if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} {
## set ns_firstword [string range $ns_firstword 1 end-1]
##}
if {$::tcl_platform(platform) ne "windows" && [string match {/*/env} $ns_firstword]} {
set exec_part $nextshellpath
} else {
set epath [auto_execok $ns_firstword]
@ -794,6 +974,10 @@ if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} {
set exec_part [list {*}$epath {*}[lrange $nextshellpath 1 end]]
}
}
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist"
puts stdout "exec: $exec_part $scrname $arglist"
catch {exec {*}$exec_part $scrname {*}$arglist <@stdin >@stdout 2>@stderr} emsg eopts
if {[dict exists $eopts -errorcode]} {
@ -837,11 +1021,6 @@ namespace eval ::punk::multishell {
}
}
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload
#puts "script : [info script]"
#puts "argcount : $::argc"
#puts "argvalues: $::argv"
#puts "argv0 : $::argv0"
# -- --- --- --- --- --- --- --- --- --- --- ---
#<tcl-payload>
puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl"
@ -867,8 +1046,11 @@ if {[::punk::multishell::is_main]} {
HEREDOC1B_HIDE_FROM_BASH_AND_SH
# Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \
echo "var0: $0 @: $@"
# use oldschool backticks and sed - lowest common denominator \
ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
# use oldschool backticks and sed (posix - lowest common denominator) \
# ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` \
# some ps impls will return arguments - so last field not always appropriate \
# some ps impls don't have -o (e.g cygwin) so ps_shellname may remain empty and emit an error \
ps_shellname=`ps -o pid,comm -p $$ | awk '$1 != "PID" {print $2}'`
# \
echo "shell from ps: $ps_shellname"
# \
@ -900,8 +1082,11 @@ pop() {
}
# ------------------------------------------------------------------------------
# non-bash-like posix diversion \
if [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ]; then
# non-bash-like posix diversion
# we don't use $BASH_VERSION/$ZSH_VERSION as these can still be set when for example
# sh is a symlink to bash (posix-mode bash - reduced bashism capabilities?)
# if our ps_shellname didn't contain a result, don't divert and risk looping
if [ -n "$ps_shellname" ] && [ "$ps_shellname" != "bash" ] && [ "$ps_shellname" != "zsh" ] ; then
shift
pop $#
eval "$POP_EXPR"
@ -919,17 +1104,14 @@ if false==false # else {
then
: #
# zsh/bash \
shift && set -- "${@:1:$((${#@}-1))}"
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
# -- 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 the %nextshell% value above
# -- if sh/bash scripting needs to run on windows too.
# -- zsh/bash script section
# --
# -- review - for zsh do we want to use: setopt KSH_ARRAYS ?
# -- arrays in bash 0-based vs 1-based in zsh
# -- stick to the @:i:len syntax which is same for both
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
plat=$(uname -s) #platform/system
@ -953,17 +1135,30 @@ elif [[ "$plat" == "CYGWIN_NT"* ]]; then
os="win32"
elif [[ "$plat" == "MSYS_NT"* ]]; then
#review..
echo MSYS
#win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#Need to consider the difference between when msys2 was launched (which strips some paths and sets up the environment)
# vs if the msys2 sh was called - (actually bash) in which case paths will be different
#wsl and cygwin or msys2 can commonly be problematic combinations - primarily due to path issues
#e.g "c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#e.g It means a /usr/bin/env bash call may launch the (linux elf) bash for wsl rather than the msys bash
#
#msys provides win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix'
#bash reports $OSTYPE msys
#there are statements around the web that cmd /c .. will work under msys2
# - but from experience, it can be required to use cmd //c ...
# or MSYS2_ARG_CONV_ECL='*' cmd /c ..
# This seems to be because process arguments that look like unix paths are converted to windows paths :/
#review!
os="win32"
#review - need ps/sed/awk to determine shell?
interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
interp=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
#use 'command -v' (shell builtin preferred over external which)
shellpath=`command -v $interp`
shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname
#"c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#This breaks calls to various unix utils such as sed etc (wsl related?)
export PATH="$shellfolder${PATH:+:${PATH}}"
elif [[ "$OSTYPE" == "win32" ]]; then
os="win32"
@ -985,6 +1180,8 @@ elif [[ "$ps_shellname" == "zsh" ]]; then
else
#fallback - doesn't seem to work in zsh - untested in early bash
IFS=$'\n' arr_oslines=($shellconfiglines)
IFS=$' \t\n'
# review
fi
nextshellpath=""
nextshelltype=""
@ -1008,8 +1205,55 @@ exitcode=0
#-- sh/bash launches nextscript here instead of shebang line at top
if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then
echo zsh/bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@"
script="$0"
if [[ "$nextshelltype" == "pwsh" || "$nextshelltype" == "powershell" ]]; then
#powershell requires the file extension to be .ps1 (always - on windows)
#on other platforms it's not required if a shebang line is used - but this script must be shebangless for portability and to maintain polyglot capabilities.
cmdpattern="[.]cmd$"
if [[ "$script" =~ $cmdpattern ]]; then
ps1script="${script%????}.ps1"
if ! cmp -s "$script" "$ps1script" ; then
#ps1script either different or missing
#on windows - batch script copies .cmd -> .ps1 if not identical
cp -f "$script" "$ps1script"
fi
script=$ps1script
fi
fi
if [[ "$plat" == "MSYS_NT"* ]]; then
#we need to deal with MSYS argument munging
cmdpattern="^cmd.exe |^cmd "
#do not double quote cmdpattern - or it will be treated as literal string
if [[ "$nextshellpath" =~ $cmdpattern ]]; then
#for now - tell the user what's going on
echo "cmd call via msys detected. performing translation of /c to //c and escaping backslashes in script path"
#flags to cmd.exe such as /c are interpreted by msys as looking like a unix path
#review - for nextshellpath targets specified in the block for win32 - we don't expect unix paths (?)
#what about other flags? - can we just double up all forward slashes?
#maint: keep this munging in sync with the tcl block and perl block which must also do msys munging
nextshellpath="${nextshellpath// \/[cC] / \/\/c }"
# echo "new nextshellpath: ${nextshellpath}"
#don't double quote this
script=${script//\\/\\\\}
fi
echo "calling ${nextshellpath} $script $@"
#load into array
cmd_array=($nextshellpath)
cmd_array+=("$script") #add script, which may contain spaces as a single entry ?
cmd_array+=( "$@" ) #add each element of args to array as a separate entry (equiv ? "${arr[@]}")
# printf "%s\n" "${cmd_array[@]}"
"${cmd_array[@]}"
# this works to make nextshellpath run - but joins $@ members incorrectly
#eval ${nextshellpath} "$script" "$@"
else
#e.g /usr/bin/env tclsh "$0" "$@"
${nextshellpath} "$0" "$@"
${nextshellpath} "$script" "$@"
fi
exitcode=$?
#echo "zsh/bash reporting exitcode: ${exitcode}"
@ -1025,7 +1269,8 @@ fi
#printf "start of bash or zsh code"
#<shell-payload>
#curl -LO https://www.gitea1.intx.com.au/jn/punkshell/raw/branch/master/getpunk.cmd
#chmod +x getpunk.cmd
git_upstream="https://gitea1.intx.com.au/jn/punkshell.git"
#review - how can we test if another url such as ssh://git@pcm-gitea1.corp.intx.com.au:2222/jn/punkshell is actually the same repo, without cloning and comparing files/history?
@ -1279,18 +1524,44 @@ if ($match.Success) {
$admininfo = $match.Groups[1].Value
$asadmin = $admininfo.Contains("asadmin=1")
if ($asadmin) {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
if ($args[0] -eq "PUNK-ELEVATED") {
# May be present if launch and elevation was done via cmd.exe script
# shift away first arg
$newargs = $args | Select-Object -Skip 1
} else {
$newargs = $args
}
# -Wait e.g for starting a service or other operations which remainder of script may depend on
$arguments = @("-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments = @("-NoProfile","-NoLogo", "-NoExit", "-ExecutionPolicy", "Bypass")
$arguments += @("-File", $($MyInvocation.MyCommand.Path))
$arguments += $args
foreach ($a in $newargs) {
if ($a -match '\s') {
$arguments += "`"$a`""
} else {
$arguments += $a
}
}
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
# If not elevated, relaunch with elevated privileges
Write-Host "Powershell elevating using start-process with -Verb RunAs"
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -Wait -Verb RunAs
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -Wait -Verb RunAs
}
Exit # Exit the current non-elevated process
} else {
if ($args[0] -eq "PUNK-ELEVATED") {
#Already elevated (by cmd.exe)
#.. but it is impossible to modify or reassign the automatic $args variable
# so let's start yet another whole new process just to remove one leading argument so the custom script can operate on parameters cleanly - thanks powershell :/
if ($PSVersionTable.PSEdition -eq 'Core') {
Start-Process -FilePath "pwsh.exe" -ArgumentList $arguments -NoNewWindow -Wait
} else {
Start-Process -FilePath "powershell.exe" -ArgumentList $arguments -NoNewWindow -Wait
}
Exit
}
}
}
}

3
src/scriptapps/getpunk.bash

@ -1,4 +1,5 @@
#curl -LO https://www.gitea1.intx.com.au/jn/punkshell/raw/branch/master/getpunk.cmd
#chmod +x getpunk.cmd
git_upstream="https://gitea1.intx.com.au/jn/punkshell.git"
#review - how can we test if another url such as ssh://git@pcm-gitea1.corp.intx.com.au:2222/jn/punkshell is actually the same repo, without cloning and comparing files/history?

7
src/scriptapps/getpunk_wrap.toml

@ -13,8 +13,11 @@
default_nextshelltype="bash"
#valid nextshelltype entries are: tcl perl powershell bash.
#nextshellpath entries must be 64 characters or less.
#nextshellpath entries must be 128 characters or less.
win32.nextshellpath="powershell -nop -nol -ExecutionPolicy ByPass -File"
#win32.nextshellpath="pwsh -nop -nol -ExecutionPolicy bypass -File"
#win32.nextshelltype="pwsh"
win32.nextshellpath="cmd.exe /c powershell -nop -nol -ExecutionPolicy ByPass -File"
win32.nextshelltype="powershell"
win32.outputfile="getpunk.cmd"

7
src/scriptapps/runtime_wrap.toml

@ -17,6 +17,11 @@
#don't use -c for launching - or in old powershell, arguments such as "a b" will become 2 arguments a b
#do use -File (even though pwsh doesn't require it)
win32.nextshellpath="powershell -nop -nol -ExecutionPolicy bypass -File"
#win32.nextshellpath="pwsh -nop -nol -ExecutionPolicy bypass -File"
#win32.nextshelltype="pwsh"
#powershell needs cmd.exe to preserve spaced args
win32.nextshellpath="cmd.exe /c powershell -nop -nol -ExecutionPolicy bypass -File"
win32.nextshelltype="powershell"
win32.outputfile="runtime.cmd"

Loading…
Cancel
Save