|
|
: "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 <inputfilepath> -outputfolder <folderpath> |
|
|
@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 |
|
|
: <<nextshell_start>> |
|
|
@SET "nextshellpath[win32___________]=powershell -nop -nol -ExecutionPolicy ByPass -File______________" |
|
|
@SET "nextshelltype[win32___________]=powershell______" |
|
|
@SET "nextshellpath[dragonflybsd____]=/usr/bin/env bash_______________________________________________" |
|
|
@SET "nextshelltype[dragonflybsd____]=bash____________" |
|
|
@SET "nextshellpath[freebsd_________]=/usr/bin/env bash_______________________________________________" |
|
|
@SET "nextshelltype[freebsd_________]=bash____________" |
|
|
@SET "nextshellpath[netbsd__________]=/usr/bin/env bash_______________________________________________" |
|
|
@SET "nextshelltype[netbsd__________]=bash____________" |
|
|
@SET "nextshellpath[linux___________]=/usr/bin/env bash_______________________________________________" |
|
|
@SET "nextshelltype[linux___________]=bash____________" |
|
|
@SET "nextshellpath[macosx__________]=/usr/bin/env bash_______________________________________________" |
|
|
@SET "nextshelltype[macosx__________]=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). |
|
|
: <<asadmin_start>> |
|
|
@SET "asadmin=0" |
|
|
: <<asadmin_end>> |
|
|
@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 =% |
|
|
@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% |
|
|
|
|
|
@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 args = "PUNK-ELEVATED " >> "%vbsGetPrivileges%" |
|
|
@ECHO For Each strArg in WScript.Arguments >> "%vbsGetPrivileges%" |
|
|
@ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%" |
|
|
@ECHO Next >> "%vbsGetPrivileges%" |
|
|
@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" |
|
|
@ECHO Launching script in new window due to administrator elevation |
|
|
@"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* |
|
|
@EXIT /B |
|
|
|
|
|
:gotPrivileges |
|
|
@REM setlocal & pushd . |
|
|
@PUSHD . |
|
|
@cd /d %~dp0 |
|
|
@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( |
|
|
@DEL "%vbsGetPrivileges%" 1>nul 2>nul |
|
|
@SET arglist=%arglist:~14% |
|
|
) |
|
|
|
|
|
: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" ( |
|
|
@SET need_ps1=1 |
|
|
) ELSE ( |
|
|
fc "%~dp0%~n0%~x0" "%~dp0%~n0.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 "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL |
|
|
) |
|
|
@REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /? |
|
|
@IF "!selected_shelltype_trimmed!"=="none" ( |
|
|
SET selected_shelltype_trimmed=pwsh |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@set argCount=30 |
|
|
@rem This is the max number of args we are willing to handle. also bounded by approx 8k char limit of cmd.exe |
|
|
@rem We do not loop over %* to count args as it is brittle for some inputs e.g will always skip cmd.exe separators e.g comma and semicolon |
|
|
@rem Set argCount higher if desired, but there is a small amount of additional looping overhead. |
|
|
|
|
|
@set tmpfile_base=%TEMP%\punkbatch_params |
|
|
@call :getUniqueFile %tmpfile_base% ".txt" paramfile |
|
|
@echo %paramfile% |
|
|
|
|
|
%= NOTE when we loop like this using the percent-n args, we lose unquoted separators such as comma and semicolon %= |
|
|
@rem https://stackoverflow.com/questions/26551/how-can-i-pass-arguments-to-a-batch-file/5493124#5493124 |
|
|
@rem outer loop required to redirect all rem lines at once to file |
|
|
@for %%x in (1) do @( |
|
|
@for /L %%f in (1,1,%argCount%) do @( |
|
|
@set "argnum=%%~nf" |
|
|
@set "a1=%%1" |
|
|
@rem @set "argname=%%!argnum!" |
|
|
@rem @echo argname: !argname! |
|
|
@call :rem_output !argnum! !a1! |
|
|
@shift |
|
|
) |
|
|
) > %paramfile% |
|
|
@echo off |
|
|
|
|
|
@set "newcommandline= " |
|
|
|
|
|
@(set target=cmd_pwsh) |
|
|
@if "%target%"=="cmd_pwsh" ( |
|
|
@for /F "delims=" %%L in (%paramfile%) do @( |
|
|
SETLOCAL DisableDelayedExpansion |
|
|
set "param=%%L" |
|
|
@REM @echo ######### %%L |
|
|
@rem call :buildcmdline newcommandline param "{" "}" |
|
|
@rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %= |
|
|
call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %= |
|
|
@rem @echo . |
|
|
) |
|
|
) ELSE ( |
|
|
@for /F "delims=" %%L in (%paramfile%) do @( |
|
|
SETLOCAL DisableDelayedExpansion |
|
|
set "param=%%L" |
|
|
call :buildcmdline newcommandline param |
|
|
) |
|
|
) |
|
|
@REM padding |
|
|
SETLOCAL EnableDelayedExpansion |
|
|
|
|
|
@echo off |
|
|
@IF EXIST %paramfile% ( |
|
|
@DEL /F /Q %paramfile% |
|
|
) |
|
|
@IF EXIST %paramfile% ( |
|
|
echo failed to delete %paramfile% |
|
|
cat %paramfile% |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@REM @SET "squoted_args=" |
|
|
@REM @for %%a in (%*) do @( |
|
|
@REM set "v=%%a" |
|
|
@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 -nol -c set-executionpolicy -Scope Process Unrestricted |
|
|
cmd /c pwsh -nop -nol -ExecutionPolicy bypass -f "%scriptrootname%.ps1" !newcommandline! |
|
|
SET task_exitcode=!errorlevel! |
|
|
) ELSE ( |
|
|
REM TODO prompt user with option to call script to install pwsh using winget |
|
|
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% |
|
|
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline! |
|
|
SET task_exitcode=!errorlevel! |
|
|
) |
|
|
) ELSE ( |
|
|
IF "!selected_shelltype_trimmed!"=="powershell" ( |
|
|
@rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% |
|
|
cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline! |
|
|
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 |
|
|
@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 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 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 |
|
|
|
|
|
: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 |
|
|
: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 |
|
|
@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 |
|
|
@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 |
|
|
# -- 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 |
|
|
# -- i.e it is a polyglot file. |
|
|
# -- 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 '@ |
|
|
#--------------------------------------------------------------------- |
|
|
#divert to configured nextshell |
|
|
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" |
|
|
foreach ln [split $scriptdata \n] { |
|
|
if {[string trim $ln] eq ""} {continue} |
|
|
if {!$in_data} { |
|
|
if {[string match ": <<nextshell_start>>*" $ln]} { |
|
|
set in_data 1 |
|
|
} |
|
|
} else { |
|
|
if {[string match "*@SET*nextshellpath?${plat}_*" $ln]} { |
|
|
set lineparts [split $ln =] |
|
|
set tail [lindex $lineparts 1] |
|
|
set nextshellpath [string trimright $tail {_"}] |
|
|
if {$nextshellpath ne "" && $nextshelltype ne ""} { |
|
|
break |
|
|
} |
|
|
} elseif {[string match "*@SET*nextshelltype?${plat}_*" $ln]} { |
|
|
set lineparts [split $ln =] |
|
|
set tail [lindex $lineparts 1] |
|
|
set nextshelltype [string trimright $tail {_"}] |
|
|
if {$nextshellpath ne "" && $nextshelltype ne ""} { |
|
|
break |
|
|
} |
|
|
} elseif {[string match ": <<nextshell_end>>*" $ln]} { |
|
|
break |
|
|
} |
|
|
} |
|
|
} |
|
|
if {$nextshelltype ne "tcl" && $nextshelltype ne "none"} { |
|
|
if {$nextshelltype in "pwsh powershell"} { |
|
|
set scrname [file rootname [info script]].ps1 |
|
|
set arglist [list] |
|
|
foreach a $::argv { |
|
|
set a "'$a'" |
|
|
lappend arglist $a |
|
|
} |
|
|
} else { |
|
|
set scrname [info script] |
|
|
set arglist $::argv |
|
|
} |
|
|
puts stdout "tclsh launching subshell of type: $nextshelltype shellpath: $nextshellpath on script $scrname with args: $arglist" |
|
|
#todo - handle /usr/bin/env |
|
|
#todo - exitcode |
|
|
if {[llength $nextshellpath] == 1 && [string index $nextshellpath 0] eq {"} && [string index $nextshellpath end] eq {"}} { |
|
|
set nextshell_words [list $nextshellpath] |
|
|
} else { |
|
|
set nextshell_words $nextshellpath |
|
|
} |
|
|
set ns_firstword [lindex $nextshellpath 0] |
|
|
if {[string index $ns_firstword 0] eq {"} && [string index $ns_firstword end] eq {"}} { |
|
|
set ns_firstword [string range $ns_firstword 1 end-1] |
|
|
} |
|
|
|
|
|
if {[string match {/*/env} $ns_firstword] && $::tcl_platform(platform) ne "windows"} { |
|
|
set 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]] |
|
|
} |
|
|
} |
|
|
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 "script : [info script]" |
|
|
#puts "argcount : $::argc" |
|
|
#puts "argvalues: $::argv" |
|
|
#puts "argv0 : $::argv0" |
|
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
|
|
|
|
#<tcl-payload> |
|
|
puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" |
|
|
#</tcl-payload> |
|
|
|
|
|
|
|
|
# -- --- --- --- --- --- --- --- --- --- --- --- |
|
|
# -- 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 - lowest common denominator \ |
|
|
ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` |
|
|
# \ |
|
|
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 \ |
|
|
if [ "$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))}" |
|
|
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### |
|
|
# -- sh/bash script section |
|
|
# -- leave as is if all that is required is launching the Tcl payload" |
|
|
# -- |
|
|
# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default |
|
|
# -- adjust the %nextshell% value above |
|
|
# -- if sh/bash scripting needs to run on windows too. |
|
|
# -- |
|
|
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### |
|
|
|
|
|
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.. |
|
|
echo MSYS |
|
|
#win32 binaries - but e.g tclsh installed in msys reports ::tcl_platform(platform) as 'unix' |
|
|
#bash reports $OSTYPE msys |
|
|
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 |
|
|
#"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" |
|
|
else |
|
|
#os="$OSTYPE" |
|
|
os="other" |
|
|
fi |
|
|
echo ostype: $OSTYPE |
|
|
## This is the sort of sed that will not work across implementations |
|
|
## shellconfiglines=$( sed -n "/: <<nextshell_start>>/{:a;n;/: <<nextshell_end>>/q;p;ba}" "$0" | grep $os) |
|
|
#awk tested on linux & freebsd |
|
|
shellconfiglines=$( awk '/^:.*<<nextshell_start>>.*$/,/^:.*<<nextshell_end>>.*$/' "$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) |
|
|
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 "$@" |
|
|
#e.g /usr/bin/env tclsh "$0" "$@" |
|
|
${nextshellpath} "$0" "$@" |
|
|
|
|
|
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" |
|
|
|
|
|
#<shell-payload> |
|
|
|
|
|
|
|
|
git_upstream="https://gitea1.intx.com.au/jn/punkshell.git" |
|
|
#review - how can we test if another url such as ssh://git@pcm-gitea1.corp.intx.com.au:2222/jn/punkshell is actually the same repo, without cloning and comparing files/history? |
|
|
|
|
|
if ! command -v git &> /dev/null; then |
|
|
echo "Git is not available. Please install git and ensure it is available on the path." |
|
|
exit 1 |
|
|
fi |
|
|
wdir="$(pwd)"; [ "$(pwd)" = "/" ] && wdir="" |
|
|
case "$0" in |
|
|
/*) scriptpath="${0}";; |
|
|
*) scriptpath="$wdir/${0#./}";; |
|
|
esac |
|
|
scriptdir="${scriptpath%/*}" |
|
|
echo "script: $0" |
|
|
echo "pwd: $(pwd)" |
|
|
echo "scriptdir: $scriptdir" |
|
|
echo "scriptpath: $scriptpath" |
|
|
scriptdir=$(realpath $scriptdir) |
|
|
scriptpath=$(realpath $scriptpath) |
|
|
|
|
|
basename=$(basename "$scriptpath") |
|
|
scriptroot="${basename%.*}" #e.g "getpunk" |
|
|
|
|
|
launchdir=$(pwd) |
|
|
if [[ "$launchdir" != "$scriptdir" ]]; then |
|
|
echo "The current directory does not seem to be the folder in which the ${scriptroot} script is located." |
|
|
read -p "Do you want to use the current directory '${launchdir}' as the location for punkshell? (Y|N)"$'\n'" Y - use launchdir ${launchdir}"$'\n'" N - use script folder '${scriptdir}'"$'\n'" (Any other value to abort): " answer |
|
|
if [[ "${answer^^}" == "Y" ]]; then |
|
|
punkfolder=$launchdir |
|
|
elif [[ "${answer^^}" == "N" ]]; then |
|
|
punkfolder=$scriptdir |
|
|
else |
|
|
exit 1 |
|
|
fi |
|
|
else |
|
|
punkfolder=$scriptdir |
|
|
fi |
|
|
cd $punkfolder |
|
|
|
|
|
contentcount=$(ls -A | wc -l) |
|
|
effectively_empty=0 |
|
|
if [ $contentcount == 0 ]; then |
|
|
effectively_empty=1 |
|
|
elif [[ ("$punkfolder" == "$scriptdir") && ("$contentcount" -lt 10) ]]; then |
|
|
#treat as empty if we have only a few files matching script root name |
|
|
count_scriptlike=$(ls ${scriptroot}.* | wc -l) |
|
|
if [ "$count_scriptlike" -eq $contentcount ]; then |
|
|
effectively_empty=1 |
|
|
fi |
|
|
fi |
|
|
|
|
|
if [ "$effectively_empty" -ne 1 ]; then |
|
|
if ! [ -d "$punkfolder/.git" ]; then |
|
|
echo "The folder '${punkfolder}' contains other items, and it does not appear to be a git project" |
|
|
echo "Please place this script in an empty folder which is to be the punkshell base folder." |
|
|
exit |
|
|
else |
|
|
repo_origin=$(git remote get-url origin) |
|
|
if [ "$repo_origin" != "$git_upstream" ]; then |
|
|
echo "The current repository origin '${repo_origin}' is not the expected upstream '${git_upstream}'" |
|
|
read -p "Continue anyway? (Y|N)" answer |
|
|
if [[ "${answer^^}" != "Y" ]]; then |
|
|
exit 1 |
|
|
fi |
|
|
fi |
|
|
fi |
|
|
fi |
|
|
|
|
|
if ! [ -d "$punkfolder/.git" ]; then |
|
|
#set defaultbranch to master to suppress loud stderr 'hint' about initial branch name. |
|
|
git -c init.DefaultBranch=master init |
|
|
git remote add origin $git_upstream |
|
|
fi |
|
|
git fetch origin |
|
|
if [[ "$punkfolder" == "$scriptdir" ]]; then |
|
|
if [ -f $scriptroot.cmd ]; then |
|
|
cp -f $scriptroot.cmd $scriptroot.cmd.lastrun |
|
|
rm $scriptroot.cmd |
|
|
fi |
|
|
fi |
|
|
git pull $git_upstream master |
|
|
git checkout $scriptroot.cmd |
|
|
git branch --set-upstream-to=origin/master master |
|
|
|
|
|
cd $launchdir #restore original CWD |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#</shell-payload> |
|
|
|
|
|
|
|
|
#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"; |
|
|
} |
|
|
|
|
|
#<perl-payload> |
|
|
print STDERR "No perl code for this script. Try another program such as tcl or bash"; |
|
|
#</perl-payload> |
|
|
|
|
|
# -- --- --- --- --- --- --- --- |
|
|
#$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 = ": <<asadmin_start>>" |
|
|
$endTag = ": <<asadmin_end>>" |
|
|
$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 (-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 |
|
|
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 |
|
|
} |
|
|
} |
|
|
} |
|
|
# |
|
|
$startTag = ": <<nextshell_start>>" |
|
|
$endTag = ": <<nextshell_end>>" |
|
|
$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 |
|
|
|
|
|
#<powershell-payload> |
|
|
|
|
|
#powershell -Command "Invoke-WebRequest -Uri 'https://www.gitea1.intx.com.au/jn/punkshell/raw/branch/master/getpunk.cmd' -OutFile 'getpunk.cmd'; Start-Process 'getpunk.cmd' -NoNewWindow -Wait" |
|
|
|
|
|
#todo - support either fossil or git |
|
|
|
|
|
#check if git available |
|
|
#if not, check/install winget, winget git |
|
|
|
|
|
$git_upstream = "https://gitea1.intx.com.au/jn/punkshell.git" |
|
|
$launchdir = Get-Location #store original CWD |
|
|
$scriptfolder = Resolve-Path (Split-Path -Path $PSCommandPath -Parent) |
|
|
$punkfolder = "" |
|
|
$scriptroot = "$([System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath))" |
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") |
|
|
|
|
|
if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) { |
|
|
Write-Host "The git command doesn't seem to be available. Will attempt to install using winget." |
|
|
#Find-Module/Install-Module: older mechanism, available in powershell |
|
|
#Find-PSResource/Install-PSResource: only available in newer pwsh etc? |
|
|
$wgclient = Get-Module -ListAvailable -Name Microsoft.WinGet.Client |
|
|
if (${wgclient}.Length -eq 0) { |
|
|
Write-Host "Microsoft.WinGet.Client module not installed.. will try to install." |
|
|
Install-PackageProvider -Name NuGet -Force |
|
|
$psgallery_existing_policy = (Get-PSRepository -Name PSGallery).InstallationPolicy |
|
|
if ($psgallery_existing_policy -eq "Untrusted") { |
|
|
#Applies to all versions of PowerShell for the user, and is persistent for current user. |
|
|
#This has risks in that a powershell session started after this call, and before we reset it, will treat PSGallery as trusted |
|
|
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted |
|
|
} |
|
|
Install-Module -Scope CurrentUser -Name Microsoft.Winget.Client -Force -Repository PSGallery |
|
|
Repair-WinGetPackageManager |
|
|
import-module -name Microsoft.Winget.client |
|
|
|
|
|
if ($psgallery_existing_policy -eq "Untrusted") { |
|
|
Set-PSRepository -Name PSGallery -InstallationPolicy Untrusted |
|
|
} |
|
|
} else { |
|
|
Write-Host "Microsoft.WinGet.Client is available" |
|
|
} |
|
|
$gitversion = (Find-WinGetPackage Git.Git).Version |
|
|
if ($gitversion) { |
|
|
Write-Host "Installing git version: ${gitversion}" |
|
|
Install-WinGetPackage -Id "Git.Git" |
|
|
} else { |
|
|
Write-Host "Failed to find git using winget" |
|
|
exit |
|
|
} |
|
|
#refreshing the current session's path should make the new command available (required for powershell 5 at least) |
|
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") |
|
|
if (Get-Command "git" -ErrorAction SilentlyContinue) { |
|
|
Write-Host "git is now available" |
|
|
} else { |
|
|
Write-Host "git is still not available" |
|
|
Write-HOst "Please install Git or relaunch your terminal and check it is available on the path." |
|
|
exit |
|
|
} |
|
|
} |
|
|
|
|
|
if (($launchdir.Path) -ne ($scriptfolder.Path)) { |
|
|
Write-Host "The current directory does not seem to be the folder in which the getpunk script is located." |
|
|
$answer = Read-Host "Do you want to use the current directory '$($launchdir.Path) as the location for punkshell? Y|N`n Y to use launchdir '$($launchdir.Path)'`n 'N' to use script folder '$($scriptfolder.Path)`n Any other value to abort: " |
|
|
if ($answer -match "y") { |
|
|
$punkfolder = $launchdir |
|
|
} elseif ($answer -match "n") { |
|
|
$punkfolder = $scriptfolder |
|
|
} else { |
|
|
exit 1 |
|
|
} |
|
|
|
|
|
} else { |
|
|
$punkfolder = $scriptfolder |
|
|
} |
|
|
$punkfoldercontents = Get-ChildItem -Path $punkfolder -Force #include possibly hidden items such as .git folder |
|
|
$contentcount = ( $punkfoldercontents | Measure-Object).Count |
|
|
$effectively_empty = 0 |
|
|
if ($contentcount -eq 0) { |
|
|
$effectively_empty = 1 |
|
|
} elseif ($punkfolder -eq $scriptfolder -and $contentcount -lt 10) { |
|
|
#treat as empty if we have only a few files matching script root name |
|
|
$scriptlike = get-childitem -Path $punkfolder | Where-Object {$_.name -like "${scriptroot}.*"} |
|
|
if ($scriptlike.Count -eq $contentcount) { |
|
|
$effectively_empty = 1 |
|
|
} |
|
|
} |
|
|
if (-not($effectively_empty)) { |
|
|
if (-not(Test-Path -Path (Join-Path -Path $punkfolder -ChildPath ".git") -PathType Container)) { |
|
|
Write-Host "The folder $punkfolder contains other items, and it does not appear to be a git project root." |
|
|
Write-Host "Please place this script in an empty folder which is to be the punkshell base folder." |
|
|
exit |
|
|
} else { |
|
|
$repo_origin = git remote get-url origin |
|
|
if ($repo_origin -ne $git_upstream) { |
|
|
Write-Host "The current repository origin '$repo_origin' is not the expected upstream '${git_upstream}'" |
|
|
$answer = Read-Host "Continue anyway? (Y|N)" |
|
|
if (-not($answer -match "y")) { |
|
|
exit 1 |
|
|
} |
|
|
} |
|
|
} |
|
|
} else { |
|
|
#punkfolder is empty, or has just the current script |
|
|
} |
|
|
Set-Location -Path $punkfolder |
|
|
|
|
|
|
|
|
if (-not(Test-Path -Path (Join-Path -Path $punkfolder -ChildPath ".git") -PathType Container)) { |
|
|
git init |
|
|
git remote add origin $git_upstream |
|
|
} |
|
|
git fetch origin |
|
|
if (($launchdir.Path) -eq ($scriptfolder.Path)) { |
|
|
if (Test-Path -Path "${scriptroot}.cmd") { |
|
|
#rename-item won't allow overwriting existing target file |
|
|
Move-Item -Path "${scriptroot}.cmd" -Destination "${scriptroot}.cmd.lastrun" -Force |
|
|
} |
|
|
} |
|
|
git pull $git_upstream master |
|
|
git checkout "${scriptroot}.cmd" |
|
|
git branch --set-upstream-to=origin/master master |
|
|
|
|
|
Set-Location $launchdir #restore original CWD |
|
|
#see also: https://github.com/jdhitsolutions/WTToolbox |
|
|
|
|
|
# Define the necessary Win32 API functions and constants |
|
|
Add-Type -TypeDefinition @" |
|
|
using System; |
|
|
using System.Runtime.InteropServices; |
|
|
|
|
|
public class WinAPI |
|
|
{ |
|
|
// Console Input/Output Handles |
|
|
public const int STD_OUTPUT_HANDLE = -11; |
|
|
public const uint ENABLE_QUICK_EDIT_MODE = 0x0040; |
|
|
public const uint ENABLE_EXTENDED_FLAGS = 0x0080; |
|
|
public const uint ENABLE_MOUSE_INPUT = 0x0010; |
|
|
public const uint ENABLE_WINDOW_INPUT = 0x0008; |
|
|
public const uint ENABLE_INSERT_MODE = 0x0020; |
|
|
public const uint ENABLE_LINE_INPUT = 0x0002; |
|
|
public const uint ENABLE_ECHO_INPUT = 0x0004; |
|
|
public const uint ENABLE_PROCESSED_INPUT = 0x0001; |
|
|
|
|
|
// Console Modes |
|
|
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; |
|
|
public const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008; |
|
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
|
public static extern IntPtr GetStdHandle(int nStdHandle); |
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
|
public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); |
|
|
[DllImport("kernel32.dll", SetLastError = true)] |
|
|
public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); |
|
|
} |
|
|
"@ |
|
|
|
|
|
# Get the handle to the console output buffer |
|
|
$stdoutHandle = [WinAPI]::GetStdHandle([WinAPI]::STD_OUTPUT_HANDLE) |
|
|
|
|
|
# Get the current console mode |
|
|
[uint32]$currentMode = 0 |
|
|
if (![WinAPI]::GetConsoleMode($stdoutHandle, [ref]$currentMode)) { |
|
|
Write-Error "Failed to get console mode. Error code: $($LAST_ERROR)" |
|
|
return |
|
|
} |
|
|
|
|
|
# Enable virtual terminal processing |
|
|
$newMode = $currentMode -bor [WinAPI]::ENABLE_VIRTUAL_TERMINAL_PROCESSING |
|
|
|
|
|
# Set the new console mode |
|
|
if (-not [WinAPI]::SetConsoleMode($stdoutHandle, $newMode)) { |
|
|
Write-Error "Failed to set console mode. Error code: $($LAST_ERROR)" |
|
|
return |
|
|
} |
|
|
|
|
|
Write-Host "Virtual terminal processing enabled successfully." |
|
|
|
|
|
write-host "`e[92m getpunk done `e[m" |
|
|
|
|
|
#</powershell-payload> |
|
|
|
|
|
|
|
|
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---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 |
|
|
# <ctrl-z> |
|
|
|
|
|
# </ctrl-z> |
|
|
# -- unreachable by tcl directly if ctrl-z character is in the <ctrl-z> 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) |
|
|
#> |
|
|
|
|
|
|
|
|
|
|
|
|