: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide x {proc $x 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, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @REM 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 @SETLOCAL EnableExtensions EnableDelayedExpansion @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___________]=powershell______________________________________________________" @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____________" : <> @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" : <> @REM @ECHO nextshelltype is %nextshelltype[win32___________]% @REM @SET "selected_shelltype=%nextshelltype[win32___________]%" @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 @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" @SET "fname=%~nx0" @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 @REM padding @REM padding @REM padding @REM padding @REM padding @REM padding @REM padding @REM padding @REM padding @REM padding @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 ) @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 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 ( pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% SET task_exitcode=!errorlevel! ) ELSE ( REM TODO prompt user with option to call script to install pwsh using winget REM powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% powershell -nop -nol -ExecutionPolicy Bypass -c "%~dp0%~n0.ps1" %arglist% SET task_exitcode=!errorlevel! ) ) ELSE ( IF "!selected_shelltype_trimmed!"=="powershell" ( powershell -nop -nol -ExecutionPolicy Bypass -c "%~dp0%~n0.ps1" %arglist% 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% "%~dp0%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 @REM boundary padding @GOTO :endlib :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 :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' @REM boundary padding @REM boundary padding @REM boundary padding @REM boundary padding @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @IF "%hasBackslash%-%hasForwardslash%"=="false-false" ( @SET "P=%cd%%~1" @CALL :getNormalizedFileTailFromPath "!P!" ftail2 @SET "result=!ftail2!" ) else ( @IF EXIST "%~1" ( @SET "result=%~nx1" ) else ( @ECHO error getNormalizedFileTailFromPath file not found: %~1 @EXIT /B 1 ) ) @SET "rtrn=%~2" @ENDLOCAL & ( @IF "%~2" neq "" ( SET "%rtrn%=%result%" ) ELSE ( @ECHO getNormalizedFileTailFromPath %1 result: %result% ) ) @EXIT /B :stringContains @REM usage: @CALL:stringContains string needle returnvarname @SETLOCAL @SET "rtrn=%~3" @SET "string=%~1" @SET "needle=%~2" @IF "!string:%needle%=!"=="!string!" @( @SET "result=false" ) ELSE ( @SET "result=true" ) @ENDLOCAL & ( @IF "%~3" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringContains %string% %needle% result: %result% ) ) @EXIT /B @REM boundary padding @REM boundary padding :stringToUpper @SETLOCAL @SET "rtrn=%~2" @SET "string=%~1" @SET "capstring=%~1" @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @( @SET "capstring=!capstring:%%A=%%A!" ) @SET "result=!capstring!" @ENDLOCAL & ( @IF "%~2" neq "" ( @SET "%rtrn%=%result%" ) ELSE ( @ECHO stringToUpper %string% result: %result% ) ) @EXIT /B :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 :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 ./filename.polypunk.cmd in sh or bash # -- e.g tclsh filename.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 '@ 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" # -- --- --- --- --- --- --- --- --- --- --- --- # puts stderr "No tcl code for this script. Try another program such as perl or bash" # # # # # # # # -- --- --- --- --- --- --- --- --- --- --- --- # -- 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 # csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ echo "script: `echo $0 | sed 's/^-//'`" # csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` #csh/tcsh diversion \ test "$argv[*]" != "[*]" && ( /usr/bin/env bash $argv[*]; exit ) #other non-bash diversion \ test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && /usr/bin/env bash $0 #review \ test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && exit # sh/bash \ shift && set -- "${@:1:$#-1}" #echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` #------------------------------------------------------ # -- This if block only needed if Tcl didn't exit or return above. if false==false # else { then : # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- 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. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### if [[ "$OSTYPE" == "linux"* ]]; then os="linux" elif [[ "$OSTYPE" == "darwin"* ]]; then os="macosx" elif [[ "$OSTYPE" == "freebsd"* ]]; then os="freebsd" elif [[ "$OSTYPE" == "dragonflybsd"* ]]; then os="dragonflybsd" elif [[ "$OSTYPE" == "netbsd"* ]]; then os="netbsd" elif [[ "$OSTYPE" == "win32" ]]; then os="win32" elif [[ "$OSTYPE" == "msys" ]]; then echo 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}}" else #os="$OSTYPE" os="other" fi echo ostype: $OSTYPE shellconfigline=$( sed -n "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) #echo $shellconfigline; if [[ $shellconfigline == *"nextshelltype"* ]]; then echo "found config for os $os" split1="${shellconfigline#*=}" #remove everything through the first '=' #echo "split1: $split1" pathraw="${split1%%\"*}" #take everything before the quote - use %% to get longest match pathraw="${pathraw//\"/}" #remove quote nextshellpath="${pathraw/%_*/}" #remove trailing underscores (% = must match at end) #echo "nextshellpath: $nextshellpath" split2="${split1#*=}" #echo "split2: $split2" split2="${split2//\"/}" nextshelltype="${split2/%_*/}" echo "nextshelltype: $nextshelltype" else echo "unable to find config for os $os" echo "shellconfigline: $shellconfigline" nextshellpath="" nextshelltype="" fi exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then #echo bash launching subshell of type $nextshelltype $nextshellpath on "$0" #/usr/bin/env tclsh "$0" "$@" ${nextshellpath} "$0" "$@" exitcode=$? #echo "sh/bash reporting exitcode: ${exitcode}" exit $exitcode #-- override exitcode example #exit 66 else #already in bash - don't launch another process or we would loop #echo "bash payload" : fi # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload #printf "start of bash or sh code" # 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 # # # # -- --- --- --- --- --- --- --- # #-- sh/bash launches Tcl here instead of shebang line at top #-- use exec to use exitcode (if any) directly from the tcl script #exec /usr/bin/env tclsh "$0" "$@" #-- alternative - can run sh/bash script after the tcl call. #/usr/bin/env tclsh "$0" "$@" #exitcode=$? #echo "sh/bash reporting tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 # # -- --- --- --- --- --- --- --- # # #printf "sh/bash done \n" # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh 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 } } # 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 # ) #} #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) { # 'myargumentname' { # Set-Variable -Name $_ -Value $PSBoundParameters."$_" # } # #... # } # foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { # #... # } # } # end { # #Main function logic # Write-Host "myargumentname value is: $myargumentname" # #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 # -- --- --- --- $startTag = ": <>" $endTag = ": <>" $fileContent = Get-Content $scriptname -Raw $pattern = "(?s)$startTag(.*?)$endTag" $matches = [regex]::Matches($fileContent,$pattern) $admininfo = $matches[0].Groups[1].Value $asadmin = 0 if ($matches.count) { $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 } } } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin 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" # # # # -- --- --- --- --- --- --- --- # #tclsh $scriptname $args #"powershell reporting exitcode: {0}" -f $LASTEXITCODE | 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) #>