: "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 + \ : "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + : << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' : STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. : shebang line is not required on unix or windows and will reduce functionality and/or portability. : Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. @GOTO :skip_perl_pod_start ^; =begin excludeperl : skip_perl_pod_start : 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, 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 ------------------------------------------------------------------------------------------------------------------------------- @rem return from endlocal macro - courtesy of jeb @rem This allows return of values containing special characters from subroutines @rem https://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal/8257951#8257951 @rem ------------------------------------------------------------------------------------------------------------------------------- @setlocal DisableDelayedExpansion @echo off %= 2 blank lines after next are required =% set LF=^ set ^"\n=^^^%LF%%LF%^%LF%%LF%^^" %= I use EDE for EnableDelayeExpansion and DDE for DisableDelayedExpansion =% set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n% setlocal EnableDelayedExpansion%\n% %= Take all variable names into the varName array =%%\n% set varName_count=0%\n% for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n% %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n% %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n =%%\n% %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n% %= The correct line will be detected by the (missing) enclosing quotes =%%\n% set "retContent=1!LF!"%\n% for /L %%n in (0 1 !varName_count!) do (%\n% for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n% set "content=!%%C!"%\n% set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n% if defined content (%\n% %= This complex block is only for replacing '!' with '^!' =%%\n% %= First replacing '"'->'""q' '^'->'^^' =%%\n% set ^"content_EDE=!content:"=""q!"%\n% set "content_EDE=!content_EDE:^=^^!"%\n% %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n% call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n% %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!' =%%\n% set "content_EDE=!content_EDE:""e=^!"%\n% %= Now restore the quotes =%%\n% set ^"content_EDE=!content_EDE:""q="!"%\n% ) ELSE set "content_EDE="%\n% set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n% )%\n% )%\n% %= Now return all variables from retContent over the barrier =%%\n% for /F "delims=" %%V in ("!retContent!") DO (%\n% %= Only the first line can contain a single 1 =%%\n% if "%%V"=="1" (%\n% %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n% endlocal%\n% endlocal%\n% ) ELSE (%\n% %= This is true in EDE =%%\n% if "!"=="" (%\n% if %%V==%%~V (%\n% %%V !%\n% )%\n% ) ELSE IF not %%V==%%~V (%\n% %%~V%\n% )%\n% )%\n% )%\n% ) else set args=" @rem ------------------------------------------------------------------------------------------------------------------------------- @SETLOCAL EnableExtensions EnableDelayedExpansion @REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system @REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder @REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) @REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash if the script is called without an interpreter - but it may depend on the shell in use when called. @REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. @REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. @REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context @SET "validshelltypes= pwsh____________ powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________ none____________" @REM for batch - only win32 is relevant - but other scripts on other platforms also parse the nextshell block to determine next shell to launch @REM nextshellpath and nextshelltype indices (underscore-padded to 16wide) are "other" plus those returned by Tcl platform pkg e.g win32,linux,freebsd,macosx @REM The horrible underscore-padded fixed-widths are to keep the batch labels aligned whilst allowing values to be set @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 : <> @SET "nextshellpath[win32___________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[win32___________]=tcl_____________" @SET "nextshellpath[dragonflybsd____]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[dragonflybsd____]=tcl_____________" @SET "nextshellpath[freebsd_________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[freebsd_________]=tcl_____________" @SET "nextshellpath[netbsd__________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[netbsd__________]=tcl_____________" @SET "nextshellpath[linux___________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[linux___________]=tcl_____________" @SET "nextshellpath[macosx__________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[macosx__________]=tcl_____________" @SET "nextshellpath[other___________]=tclsh___________________________________________________________________________________________________________________________" @SET "nextshelltype[other___________]=tcl_____________" : <> @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). : <> @SET "asadmin=0" : <> @SET "selected_shelltype=%nextshelltype[win32___________]%" @REM @ECHO selected_shelltype %selected_shelltype% @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 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 @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @REM -- cmd/batch file section (ignored on unix but should be left in place) @REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) @REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. @REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 @REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. @REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 @REM ############################################################################################################################ @REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) @REM -- Even something as simple as adding or removing an @REM @REM -- From within punkshell - use: @REM -- deck scriptwrap.checkfile filepath @REM -- to check your templates or final wrapped scripts for byte boundary issues @REM -- It will report any labels that are on boundaries @REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. @REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. @REM -- Alternatively, as you should do anyway - test the final script on windows @REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. @REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. @REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label @REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. @REM -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided @REM ############################################################################################################################ @REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @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 @REM @ECHO commandlineresolved %~f0 @CALL :getNormalizedScriptTail nftail @REM @ECHO normalizedscripttail %nftail% @CALL :getFileTail %0 clinetail @REM @ECHO clinetail %clinetail% @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" ) else ( @CALL :stringToUpper %clinetail% capcmdlinetail @REM @ECHO capcmdlinetail !capcmdlinetail! IF "%clinetail%"=="!capcmdlinetail!" ( @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase @set "asadmin=1" ) ) @SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" @SET arglist=%* @SET "qstrippedargs=args%arglist%" @SET "qstrippedargs=%qstrippedargs:"=%" @IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( GOTO :gotPrivileges ) @IF !asadmin!==1 ( net file 1>NUL 2>NUL @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) ) @REM padding @GOTO skip_privileges :getPrivileges @IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) @ECHO Set UAC = CreateObject^("Shell.Application"^) > "%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 ^& 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 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 %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 "%scriptrootname%.ps1" ( @SET need_ps1=1 ) ELSE ( fc "%fullscriptname%" "%scriptrootname%.ps1" >nul || goto different @REM @ECHO "files same" @SET need_ps1=0 ) @GOTO :pscontinue :different @REM @ECHO "files differ" @SET need_ps1=1 :pscontinue @IF !need_ps1!==1 ( 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 ) @REM @SET "squoted_args=" @REM @for %%a in (%*) do @( @REM set "v=%%a" @REM set "v=!v:'=''!" @REM SET "squoted_args=!squoted_args!'!v!' " @REM ) @REM @SET "squoted_args=%squoted_args:~0,-1%" @REM @ECHO %squoted_args% @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 -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 %= 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" ( %= 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" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% SET task_exitcode=!errorlevel! ) ELSE ( REM perl or tcl or sh or bash IF NOT "x%keyRemoved%"=="x%validshelltypes%" ( 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 @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 ) ) ) ) @REM batch file library functions @GOTO :endlib @REM padding @REM padding @REM padding @REM padding %= ---------------------------------------------------------------------- =% @rem courtesy of dbenham :: Example usage @rem call :getUniqueFile "d:\test\myFile" ".txt" myFile @rem echo myFile="%myFile%" :getUniqueFile baseName extension rtnVar setlocal :getUniqueFileLoop for /f "skip=1" %%A in ('wmic os get localDateTime') do for %%B in (%%A) do set "rtn=%~1_%%B%~2" if exist "%rtn%" ( goto :getUniqueFileLoop ) else ( 2>nul >nul (9>"%rtn%" timeout /nobreak 1) || goto :getUniqueFileLoop ) endlocal & set "%~3=%rtn%" exit /b %= ---------------------------------------------------------------------- =% @REM padding :buildcmdline cmdlinevar paramvar wrapA wrapB %= quoting for cmd.exe /c pwsh -nop !args! =% @SETLOCAL EnableDelayedExpansion @REM @echo ===================== set "pval=!%~2:*#=!" set "pval=!pval:~0,-2!" @REM set "pval=!pval:~0,-1!" set "wrapa=%~3" set "wrapb=%~4" @call :strlen pval slen @rem @echo strlen: !slen! if "!slen!"=="0" ( goto :eof ) @set /A n = !slen! - 1 @(set str=) @set "dq="" @set "bang=^!" @(set carat=^^) @for /l %%i in (0,1,!n!) do @( (set c=!pval:~%%i,1!) if "!c!"=="|" ( set "ch=^^!pval:~%%i,1!" ) ELSE IF "!c!"=="(" ( set "ch=^(" ) ELSE if "!c!"==")" ( set "ch=^)" ) ELSE if "!c!"=="&" ( set "ch=^^&" ) ELSE if "!c!"=="!dq!" ( set "ch=^"" ) ELSE if "!c!"=="!bang!" ( @rem - double caret - first for initial parsing, second to ensure remains escaped during delayed expansion phase @rem - REVIEW set "ch=^^!bang!" ) ELSE if "!c!"=="^carat" ( set "ch=^^^^" ) ELSE if "!c!"=="'" ( set "ch=''" ) ELSE ( set "ch=!c!" ) @rem @echo - !ch! set "str=!str!!ch!" ) echo +++++ %~1 = !%1! !str! set "%~1=!%1! !wrapa!!str!!wrapb!" @rem old method of return - failes to preserve exclamation marks @rem for /f "delims=" %%A in (""!str!"") do endlocal & set "%~1=!%1! '%%~A'" @rem macro method of endlocal return - preserving !val! @echo off %endlocal% %~1 @exit /b :rem_output @rem --------------------------------------------- @rem rem_output is called for each n in the number of args we process - as we don't have a non-destructive way to count args whilst accepting special chars @rem we therefore need a way to stop processing on the last received arg so we don't write argCount entries to param.txt if less are received @rem see 'disappearing quotes' technique @rem https://stackoverflow.com/questions/4643376/how-to-split-double-quoted-line-into-multiple-lines-in-windows-batch-file/4645113#4645113 @rem and @rem https://groups.google.com/g/alt.msdos.batch.nt/c/J71F17Bve9Y (sponge belly) @echo off setlocal enableextensions disabledelayedexpansion set "param1=%~2" rem do must not be indented for %%^" in ("") ^ do if not defined param1 set %%~"param1=%2%%~" if not defined param1 goto :eof endlocal @rem --------------------------------------------- @PROMPT @ @echo on rem %1 #%2# @exit /b @rem padding @REM courtesy of: https://stackoverflow.com/users/463115/jeb :strlen stringVar returnVar @( setlocal EnableDelayedExpansion @SET "rtrn=%~2" (set^ tmp=!%~1!) @rem @echo jjjjj !tmp! @if defined tmp ( set "len=1" @for %%P in (4096 2048 1024 512 256 128 64 32 16 8 4 2 1) do @( @if "!tmp:~%%P,1!" NEQ "" ( set /a "len+=%%P" set "tmp=!tmp:~%%P!" ) ) ) ELSE ( set len=0 ) ) @( endlocal @IF "%~2" neq "" ( @SET "%rtrn%=%len%" ) ELSE ( @ECHO :strlen result: %len% ) exit /b ) :getWslPath @SETLOCAL @SET "_path=%~p1" @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" @REM Although drive letters on windows are normally upper case wslbash seems to expect lower case drive letters @CALL :stringToLower %drive ldrive @SET "result=/mnt/%ldrive:~0,1%%_path:\=/%%name%" @ENDLOCAL & ( @if "%~2" neq "" ( SET "%rtrn%=%result%" ) ELSE ( ECHO %result% ) ) @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 @REM This function is designed explicitly to preserve capitalisation @REM accepts full paths with either / or \ as delimiters - or @SETLOCAL @SET "rtrn=%~2" @SET "arg=%~1" @REM @SET "result=%_arg:*/=%" @REM @SET "result=%~1" @SET LF=^ : The above 2 empty lines are important. Don't remove @CALL :stringContains "!arg!" "\" hasBackSlash @IF "!hasBackslash!"=="true" ( @for %%A in ("!LF!") do @( @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B" ) ) ELSE ( @CALL :stringContains "!arg!" "/" hasForwardSlash @IF "!hasForwardSlash!"=="true" ( @FOR %%A in ("!LF!") do @( @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B" ) ) ELSE ( @set "result=%arg%" ) ) @ENDLOCAL & ( @if "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding @REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @SET "rtrn=%~1" @ENDLOCAL & ( @IF "%~1" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding @REM boundary padding :getNormalizedFileTailFromPath @REM warn via echo, and do not set return variable if path not found @REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @IF "%hasBackslash%-%hasForwardslash%"=="false-false" ( @SET "P=%cd%%~1" @CALL :getNormalizedFileTailFromPath "!P!" ftail2 @SET "result=!ftail2!" ) else ( @IF EXIST "%~1" ( @SET "result=%~nx1" ) else ( @ECHO error getNormalizedFileTailFromPath file not found: %~1 @EXIT /B 1 ) ) @SET "rtrn=%~2" @ENDLOCAL & ( @IF "%~2" neq "" ( SET "%rtrn%=%result%" ) ELSE ( @ECHO getNormalizedFileTailFromPath %1 result: %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding @REM boundary padding :stringContains @REM usage: @CALL:stringContains string needle returnvarname @SETLOCAL @SET "rtrn=%~3" @SET "string=%~1" @SET "needle=%~2" @IF "!string:%needle%=!"=="!string!" @( @SET "result=false" ) ELSE ( @SET "result=true" ) @ENDLOCAL & ( @IF "%~3" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringContains %string% %needle% result: %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding :stringToUpper strvar returnvar @SETLOCAL @SET "rtrn=%~2" @SET "string=%~1" @SET "capstring=%~1" @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @( @SET "capstring=!capstring:%%A=%%A!" ) @SET "result=!capstring!" @ENDLOCAL & ( @IF "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringToUpper %string% result: %result% ) ) @EXIT /B :stringToLower @SETLOCAL @SET "rtrn=%~2" @SET "string=%~1" @SET "retstring=%~1" @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( @SET "retstring=!retstring:%%A=%%A!" ) @SET "result=!retstring!" @ENDLOCAL & ( @IF "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringToLower %string% result: %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding @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 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:________###=###%" @SET "trimstring=%trimstring:____###=###%" @SET "trimstring=%trimstring:__###=###%" @SET "trimstring=%trimstring:_###=###%" @SET "trimstring=%trimstring:###=%" @SET "result=!trimstring!" @ENDLOCAL & ( @IF "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringTrimTrailingUnderscores %string% result: %result% ) ) @EXIT /B :isNumeric @SETLOCAL @SET "notnumeric="&FOR /F "delims=0123456789" %%i in ("%1") do set "notnumeric=%%i" @IF defined notnumeric ( @SET "result=false" ) else ( @SET "result=true" ) @SET "rtrn=%~2" @ENDLOCAL & ( @IF "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO %result% ) ) @EXIT /B :endlib : \ @REM padding @REM padding @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell @GOTO :exit_multishell # } # -*- tcl -*- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- tcl script section # -- This is a punk multishell file # -- 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 # -- e.g tclsh scriptname.cmd # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### 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] #may be old tcl - not assuming readFile available set fd [open [info script] r] set scriptdata [read $fd] close $fd set scriptdata [string map [list \r\n \n] $scriptdata] 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} { if {[string match ": <>*" $ln]} { set in_data 1 } } else { 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?${os}_*" $ln]} { set lineparts [split $ln =] set tail [lindex $lineparts 1] set nextshelltype [string trimright $tail {_"}] if {$nextshellpath ne "" && $nextshelltype ne ""} { break } } elseif {[string match ": <>*" $ln]} { break } } } if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} { set script_rootname [file rootname $script_as_called] if {$nextshelltype in "pwsh powershell"} { # 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 $script_as_called set arglist $::argv } #todo - handle /usr/bin/env #todo - exitcode #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 { lappend new_nextshellpath $w } } set nextshellpath $new_nextshellpath } } } 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] if {$epath eq ""} { error "unable to find executable path for first word '$ns_firstword' of nextshellpath '$nextshellpath'" } else { 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]} { set ecode [dict get $eopts -errorcode] if {[lindex $ecode 0] eq "CHILDSTATUS"} { exit [lindex $ecode 2] } else { puts stderr "error calling next shell $nextshelltype :" puts stderr $emsg exit 1 } } else { exit 0 } } #--------------------------------------------------------------------- namespace eval ::punk::multishell { set last_script_root [file dirname [file normalize ${::argv0}/__]] set last_script [file dirname [file normalize [info script]/__]] if {[info exists ::argv0] && $last_script eq $last_script_root } { set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode } else { set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. } if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { proc ::punk::multishell::is_main {{script_name {}}} { if {$script_name eq ""} { set script_name [file dirname [file normalize [info script]/--]] } if {![info exists ::punk::multishell::is_main($script_name)]} { #e.g a .dll or something else unanticipated puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting" puts stderr "Info: script_root: [file dirname [file normalize ${::argv0}/__]]" return 0 } return [set ::punk::multishell::is_main($script_name)] } } } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload # puts stdout "::argc" puts stdout $::argc puts stdout "::argv" puts stdout "$::argv" puts stdout ----------------------- foreach a $::argv { puts stdout $a } puts stdout -done- # # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. # -- If the multishell script is modified to have Tcl below the Tcl Payload section, # -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. # -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below # -- but the sh/bash 'then' and 'fi' would also need to be uncommented. # -- This facility left in place for experiments on whether configuration payloads etc can be appended # -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells # -- can be made to ignore/cope with such data. if {[::punk::multishell::is_main]} { exit 0 } else { return } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload # end hide from unix shells \ HEREDOC1B_HIDE_FROM_BASH_AND_SH # 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 (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" # \ 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 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))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- 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 if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" elif [[ "$plat" == "FreeBSD"* ]]; then os="freebsd" elif [[ "$plat" == "DragonFly"* ]]; then os="dragonflybsd" elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" elif [[ "$plat" == "MINGW32"* ]]; then os="win32" elif [[ "$plat" == "MINGW64"* ]]; then os="win32" elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #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/^.*\/|^-//'` #use 'command -v' (shell builtin preferred over external which) shellpath=`command -v $interp` shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname export PATH="$shellfolder${PATH:+:${PATH}}" elif [[ "$OSTYPE" == "win32" ]]; then os="win32" else #os="$OSTYPE" os="other" fi echo ostype: $OSTYPE ## This is the sort of sed that will not work across implementations ## shellconfiglines=$( sed -n "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) #awk tested on linux & freebsd shellconfiglines=$( awk '/^:.*<>.*$/,/^:.*<>.*$/' "$0" | grep $os) # echo $shellconfiglines; # readarray requires bash 4.0 if [[ "$ps_shellname" == "bash" ]]; then readarray -t arr_oslines <<<"$shellconfiglines" elif [[ "$ps_shellname" == "zsh" ]]; then arr_oslines=("${(f)shellconfiglines}") 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="" for ln in "${arr_oslines[@]}"; do # echo "---- $ln" if [[ "$ln" == *"nextshellpath"* ]]; then splitln="${ln#*=}" #remove everything through the first '=' 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" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" # 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 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} "$script" "$@" fi exitcode=$? #echo "zsh/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 "zsh/bash payload" : fi # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload #printf "start of bash or zsh code" # echo "No bash code for this script. Try another program such as perl or tcl" >&2 # #printf "zsh/bash done \n" # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- Perl script section # -- leave the script below as is, if all that is required is launching the Tcl payload" # -- # -- Note that perl script isn't called by default when simply running this script by name # -- adjust the nextshell value at the top of the script to point to perl # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### =cut #!/user/bin/perl my $exit_code = 0; use Cwd qw(abs_path); my $scriptname = abs_path($0); #print "perl $scriptname\n"; my $os = "$^O"; if ($os eq "MSWin32") { $os = "win32"; } elsif ($os eq "darwin") { $os = "macosx"; } print "os $os\n"; # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload #use ExtUtils::Installed; #my $installed = ExtUtils::Installed->new(); #my @modules = $installed->modules(); #print "Modules:\n"; #foreach my $m (@modules) { # print "$m\n"; #} # -- --- --- my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; } # print STDERR "No perl code for this script. Try another program such as tcl or bash"; # # -- --- --- --- --- --- --- --- #$exit_code=system("tclsh", $scriptname, @ARGV); #print "perl reporting tcl exitcode: $exit_code"; # -- --- --- --- --- --- --- --- # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload exit $exit_code; __END__ # end hide sh/bash/perl block from Tcl # This comment with closing brace should stay in place whether if commented or not } #------------------------------------------------------ # begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above if 0 { : end heredoc1 - end hide from powershell \ '@ # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- powershell/pwsh section # -- Do not edit if current file is the .ps1 # -- Edit the corresponding .cmd and it will autocopy # -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above # -- custom script should generally go below the begin_powershell_payload line # ## ### ### ### ### ### ### ### ### ### ### ### ### ### function GetScriptName { $myInvocation.ScriptName } $scriptname = GetScriptName function GetDynamicParamDictionary { [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true, Mandatory=$true)] [string] $CommandName ) begin { # Get a list of params that should be ignored (they're common to all advanced functions) $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name } process { # Create the dictionary that this scriptblock will return: $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary # Convert to object array and get rid of Common params: (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | Where-Object { $CommonParameterNames -notcontains $_.Key } | ForEach-Object { $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( $_.Key, $_.Value.ParameterType, $_.Value.Attributes ) $DynParamDictionary.Add($_.Key, $DynamicParameter) } # Return the dynamic parameters return $DynParamDictionary } } # Example usage: # GetDynamicParamDictionary # - This can make it easier to share a single set of param definitions between functions # - sample usage #function ParameterDefinitions { # param( # [Parameter(Mandatory)][string] $myargument, # [Parameter(ValueFromRemainingArguments)] $opts # ) #} #function psmain { # [CmdletBinding()] # param() # dynamicparam { GetDynamicParamDictionary ParameterDefinitions } # process { # #called once with $PSBoundParameters dictionary # #can be used to validate arguments, or set a simpler variable name for access # switch ($PSBoundParameters.keys) { # 'myargument' { # Set-Variable -Name $_ -Value $PSBoundParameters."$_" # } # 'opts' { # write-warning "Unused parameters: $($PSBoundParameters.$_)" # } # Default { # write-warning "Unhandled parameter -> [$($_)]" # } # } # foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { # #... # } # } # end { # #Main function logic # Write-Host "myargument value is: $myargument" # #myotherfunction @PSBoundParameters # } #} #psmain @args #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host "Script Name : {0}" -f $scriptname | write-host "Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host "powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- $thisfileContent = Get-Content $scriptname -Raw $startTag = ": <>" $endTag = ": <>" $pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" $match = [regex]::Match($thisfileContent,$pattern) $asadmin = 0 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 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 } } } } # $startTag = ": <>" $endTag = ": <>" $pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" $match = [regex]::Match($thisfileContent,$pattern) if ($match.Success) { $plat = [System.Environment]::OSVersion.Platform if ($plat -eq "Unix") { $runtime_ident = [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier switch ($runtime_ident.split("-")[0]) { "freebsd" { # untested $os = "freebsd" } "linux" { $os = "linux" } "osx" { # osx-x64 or osx-arm64 ? $os = "macosx" } default { #openbsd, netbsd ? $os = "other" } } } else { $os = "win32" } $matchedlines = $match.Groups[1].Value $nextshell_type = "" $nextshell_path = "" ForEach ($line in $($matchedlines -split "\r?\n")) { $m = [regex]::Match($line,".*nextshelltype\[${os}[_]+\]=([^_]*)[_]*") if ($m.Success) { $nextshell_type = $m.Groups[1].Value } $m = [regex]::Match($line,".*nextshellpath\[${os}[_]+\]=([^_]*)[_]*") if ($m.Success) { $nextshell_path = $m.Groups[1].Value } if ($nextshell_type -ne "" -and $nextshell_path -ne "") { break } } if (-not (("pwsh", "powershell", "") -contains $nextshell_type)) { #nextshell diversion exists for this platform write-host "os: $os pwsh/powershell launching subshell of type: $nextshell_type shellpath: $nextshell_path on script $scriptname" # $arguments = @($($MyInvocation.MyCommand.Path)) # $arguments += $args # NOTE - this gives incorrect argument quoting e.g wrong number of arguments received by launched process for arguments: a "b c" # $process = (Start-Process -FilePath $nextshell_path -ArgumentList $arguments -NoNewWindow -Wait) # Exit $process.ExitCode & $nextshell_path $scriptname $args exit $LASTEXITCODE } } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload # Write-Error "No powershell code for this script. Try another program such as tcl or bash`n" "powershell args : {0}" -f ($args -join ", ") | write-host # # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' ' : comment end hide powershell-block from Tcl \ # This comment with closing brace should stay in place whether 'if' commented or not } : multishell doubled-up cmd exit label - return exitcode :exit_multishell :exit_multishell : \ @REM @ECHO exitcode: !task_exitcode! : \ @IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) : \ @EXIT /B !task_exitcode! # cmd has exited : comment end heredoc2 \ '@ <# # id:tailblock0 # -- powershell multiline comment #> <# no script engine should try to run me # id:tailblock1 #  # # -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A) # -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data # -- so for example a plain text tar archive could cause problems depending on the content. # -- final line in file must be the powershell multiline comment terminator or other data it can handle. # -- e.g plain # comment lines will work too # -- (for example a powershell digital signature is a # commented block of data at the end of the file) #>