@ -1,4 +1,4 @@
: " punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell " + " [rename set S;proc Hide shell_not_supported {proc $ shell_not_supported args {}};Hide :] " + " \ $( function : { <# pwsh #> } ) " + " perlhide " + qw ^
: " punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/ bash cmd pwsh powershell " + " [rename set S;proc Hide shell_not_supported {proc $ shell_not_supported args {}};Hide :] " + " \ $( function : { <# pwsh #> } ) " + " perlhide " + qw ^
set - - " $ @ " " a=[Hide <#;Hide set;S 1 list] " ; set - - : " $ @ " ; $1 = @ '
: heredoc1 - hide from powershell using @ and squote above . close sqote for unix shells + ' \
: . bat / . cmd launch section , leading colon hides from cmd , trailing slash hides next line from tcl + \
@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @'
: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \
: {
@REM ############################################################################################################################
@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl , bash , ( some sh ) and / or powershelll ( powershell . exe or pwsh . exe )
@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl , zsh , bash , ( sh diversion ) and / or powershelll ( powershell . exe or pwsh . exe )
@REM It should remain portable between unix-like OSes & windows if the proper structure is maintained .
@REM ############################################################################################################################
@rem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@ -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% "
@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,73 +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 " { " " } "
call : buildcmdline newcommandline param ' ' % = cmd . exe / c powershell % =
@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 "
@ -303,25 +381,31 @@ SETLOCAL EnableDelayedExpansion
@IF " !selected_shelltype_trimmed! " = = " pwsh " (
REM pwsh vs powershell hasn 't been tested because we didn' t need to copy cmd to ps1 this time
REM test availability of preferred option of powershell7 + pwsh
REM when run without cmd . exe - pwsh will receive the semicolon ( for cmd . exe unquoted semicolon and comma are separators that aren ' t seen in positional arguments )
pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted 2 > NUL ; write-host " statusmessage: pwsh-found " > NUL
SET pwshtest_exitcode = ! errorlevel !
REM ECHO pwshtest_exitcode ! pwshtest_exitcode !
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 -c " %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 -c " %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 -c " %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 " (
@ -335,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
@ -373,6 +456,7 @@ endlocal & set "%~3=%rtn%"
exit / b
% = - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - = %
@REM padding
: buildcmdline cmdlinevar paramvar wrapA wrapB
% = quoting for cmd . exe / c pwsh -nop ! args ! = %
@SETLOCAL EnableDelayedExpansion
@ -455,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
@ (
@ -503,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
@ -542,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 "
@ -650,13 +738,15 @@ 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:________###=###% "
@ -702,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
@ -755,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 {
#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 [info script]
set scrname $script_as_called
set arglist $ :: argv
}
puts stdout " tclsh launching subshell of type: $ nextshelltype shellpath: $ nextshellpath on script $ scrname with args: $ arglist "
@ -842,16 +983,6 @@ namespace eval ::punk::multishell {
puts stderr " No tcl code for this script. Try another program such as zsh or bash or perl "
#</tcl-payload>
#<tcl-pre-launch-subprocess>
#</tcl-pre-launch-subprocess>
#<tcl-launch-subprocess>
#</tcl-launch-subprocess>
#<tcl-post-launch-subprocess>
#</tcl-post-launch-subprocess>
# -- --- --- --- --- --- --- --- --- --- --- ---
# -- Best practice is to always return or exit above, or just by leaving the below defaults in place.
@ -872,28 +1003,67 @@ 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 @: $ @ "
# echo "script: `echo $0 | sed 's/^-//'`"
# use oldschool backticks and sed - lowest common denominator \
# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'`
# zsh diversion \
# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi
# \
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 argc: $ {#@} inner: $ {@:2: $( ( $ { #@}-2))}"
# non-bash-like diversion \
if [ [ " $ ps_shellname " ! = " bash " & & " $ ps_shellname " ! = " zsh " ] ] ; then / usr / bin / env bash " $ 0 " " $ {@:2: $( ( $ { #@}-2))}"; exit $?; fi
# sh/bash (or zsh?) \
shift & & set - - " $ {@:1: $( ( $ { #@}-1))}"
# \
#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'`
echo " shell from ps: $ ps_shellname "
# \
echo " args: $ @ "
# ------------------------------------------------------------------------------
# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above.
if false = = false # else {
then
: #
# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic)
# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix
# posix compliant pop
pop ( ) {
__pop_n = $ ( ( $1 - $ { 2 : - 1 } ) )
if [ -n " $ ZSH_VERSION " -o -n " $ BASH_VERSION " ] ; then
POP_EXPR = 'set -- "${@:1:' $__pop_n '}"'
elif [ $__pop_n -ge 500 ] ; then
POP_EXPR = " set -- $( seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g' ) "
else
__pop_index = 0
__pop_arguments = " "
while [ $__pop_index -lt $__pop_n ] ; do
__pop_index = $ ( ( __pop_index + 1 ) )
__pop_arguments = " $ __pop_arguments \ " \ $ { $__pop_index } \ " "
done
POP_EXPR = " set -- $ __pop_arguments "
fi
}
# ------------------------------------------------------------------------------
# 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 "
echo " divert to bash $ 0 $ @ "
/ usr / bin / env bash " $ 0 " " $ @ "
exit $ ?
fi
# close false==false block
fi
# close tcl wrap }
# ------------------------------------------------------
# -- This if block only needed if Tcl didn't exit or return above.
# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above.
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"
@ -905,7 +1075,7 @@ if false==false # else {
# ## ### ### ### ### ### ### ### ### ### ### ### ### ###
plat = $ ( uname -s ) #platform/system
if [ [ " $ plat " = " Linux " * ] ] ; then
if [ [ " $ plat " = = " Linux " * ] ] ; then
os = " linux "
elif [ [ " $ plat " = = " Darwin " * ] ] ; then
os = " macosx "
@ -917,25 +1087,39 @@ elif [[ "$plat" == "NetBSD"* ]]; then
os = " netbsd "
elif [ [ " $ plat " = = " OpenBSD " * ] ] ; then
os = " openbsd "
elif [ [ " $ plat " = " MINGW32 " * ] ] ; then
elif [ [ " $ plat " = = " MINGW32 " * ] ] ; then
os = " win32 "
elif [ [ " $ plat " = " MINGW64 " * ] ] ; then
elif [ [ " $ plat " = = " MINGW64 " * ] ] ; then
os = " win32 "
elif [ [ " $ plat " = " CYGWIN_NT " * ] ] ; 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 "
@ -967,34 +1151,72 @@ for ln in "${arr_oslines[@]}"; do
pathraw = " $ {splitln%%\ " * } " #take everything before the quote - use %% to get longest match
#remove trailing underscores (% means must match at end)
nextshellpath = " $ {pathraw/%_*/} "
echo " nextshellpath: $ nextshellpath "
# echo "nextshellpath: $nextshellpath "
elif [ [ " $ ln " = = * " nextshelltype " * ] ] ; then
splitln = " $ {ln#*=} "
typeraw = " $ {splitln%%\ " * } "
nextshelltype = " $ {typeraw/%_*/} "
echo " nextshelltype: $ nextshelltype "
# echo "nextshelltype: $nextshelltype "
fi
done
exitcode = 0
#-- sh/bash launches nextscript here instead of shebang line at top
if [ [ " $ nextshelltype " ! = " bash " & & " $ nextshelltype " ! = " none " ] ] ; then
echo bash launching subshell of type : $nextshelltype shellpath : $nextshellpath on " $ 0 " with args " $ @ "
#e.g /usr/bin/env tclsh "$0" "$@"
$ { nextshellpath } " $ 0 " " $ @ "
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 } " $ script " " $ @ "
fi
exitcode = $ ?
#echo "sh/bash reporting exitcode: ${exitcode}"
#echo "z sh/bash reporting exitcode: ${exitcode}"
exit $exitcode
#-- override exitcode example
#exit 66
else
#already in bash - don't launch another process or we would loop
#echo "bash payload"
#echo "zsh/ bash payload"
:
fi
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload
#printf "start of bash or sh code"
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin z sh Payload
#printf "start of bash or z sh code"
#<shell-payload>
@ -1090,29 +1312,9 @@ cd $launchdir #restore original CWD
#</shell-payload>
#<shell-pre-launch-subprocess>
#</shell-pre-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<shell-launch-subprocess>
#-- sh/bash launches Tcl here instead of shebang line at top
#-- use exec to use exitcode (if any) directly from the tcl script
#exec /usr/bin/env tclsh "$0" "$@"
#-- alternative - can run sh/bash script after the tcl call.
#/usr/bin/env tclsh "$0" "$@"
#exitcode=$?
#echo "sh/bash reporting tcl exitcode: ${exitcode}"
#-- override exitcode example
#exit 66
#</shell-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<shell-post-launch-subprocess>
#</shell-post-launch-subprocess>
#printf "sh/bash done \n"
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload
#printf "zsh/bash done \n"
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload
#------------------------------------------------------
fi
exit $ { exitcode }
@ -1148,7 +1350,6 @@ print "os $os\n";
# -- --- ---
my $i = 1 ;
foreach my $a ( @ARGV ) {
print " Arg # $ i: $ a\n " ;
@ -1158,21 +1359,11 @@ foreach my $a(@ARGV) {
print STDERR " No perl code for this script. Try another program such as tcl or bash " ;
#</perl-payload>
#<perl-pre-launch-subprocess>
#</perl-pre-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<perl-launch-subprocess>
#$exit_code=system("tclsh", $scriptname, @ARGV);
#print "perl reporting tcl exitcode: $exit_code";
#</perl-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<perl-post-launch-subprocess>
#</perl-post-launch-subprocess>
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload
exit $exit_code ;
@ -1282,18 +1473,44 @@ if ($match.Success) {
$admininfo = $match . Groups [ 1 ] . Value
$asadmin = $admininfo . Contains ( " asadmin=1 " )
if ( $asadmin ) {
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 " , " -NoLogo " , " -NoExit " , " -ExecutionPolicy " , " Bypass " )
$arguments + = @ ( " -File " , $ ( $MyInvocation . MyCommand . Path ) )
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
# -Wait e.g for starting a service or other operations which remainder of script may depend on
$arguments = @ ( " -NoProfile " , " -NoExit " , " -ExecutionPolicy " , " Bypass " )
$arguments + = @ ( " -File " , $ ( $MyInvocation . MyCommand . Path ) )
$arguments + = $args
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
}
}
}
}
@ -1540,20 +1757,6 @@ write-host "`e[92m getpunk done `e[m"
#</powershell-payload>
#<powershell-pre-launch-subprocess>
#</powershell-pre-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<powershell-launch-subprocess>
#tclsh $scriptname $args
#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host
#</powershell-launch-subprocess>
# -- --- --- --- --- --- --- ---
#<powershell-post-launch-subprocess>
#</powershell-post-launch-subprocess>
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload
Exit $LASTEXITCODE