set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide s {proc $s args {}}; Hide :;rename set s2;Hide set;s2 1 list]"; set -- : "$@"; $1 = @' : heredoc1 - hide from powershell (close sqote for unix shells) ' \ : << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \ : "[Hide @ECHO; Hide ); Hide (;Hide echo]#not necessary but can help avoid errs in testing" : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ @REM { @REM DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required and will reduce functionality. @REM Even comment lines can be part of the functionality of this script - modify with care. @REM Change the value of nextshell in the next line if desired, and code within payload sections as appropriate. @SET "nextshell=pwsh" @REM nextshell set to pwsh,sh,bash or tclsh @REM @ECHO nextshell is %nextshell% @SET "validshells=pwsh,sh,bash,tclsh" @CALL SET keyRemoved=%%validshells:%nextshell%=%% @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) @REM -- This section intended only to launch the next shell @REM -- Avoid customising this if possible. cmd/batch script is probably the least expressive language. @REM -- custom windows payloads should be in powershell,tclsh or sh/bash code sections @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @SETLOCAL EnableExtensions EnableDelayedExpansion @SET "winpath=%~dp0" @SET "fname=%~nx0" @REM @ECHO fname %fname% @REM @ECHO winpath %winpath% @IF %nextshell%==pwsh ( CALL pwsh -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL REM test availability of preferred option of powershell7+ pwsh CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL SET pwshtest_exitcode=!errorlevel! REM ECHO pwshtest_exitcode !pwshtest_exitcode! IF !pwshtest_exitcode!==0 CALL pwsh -nop -nol "%~dp0%~n0.ps1" %* & SET task_exitcode=!errorlevel! REM fallback to powershell if pwsh failed IF NOT !pwshtest_exitcode!==0 ( REM CALL powershell -nop -nol -c write-host powershell-found CALL powershell -nop -nol -file "%~dp0%~n0.ps1" %* SET task_exitcode=!errorlevel! ) ) ELSE ( IF %nextshell%==bash ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" CALL %nextshell% "!wslpath!%fname%" %* & SET task_exitcode=!errorlevel! ) ELSE ( REM probably tclsh or sh IF NOT "x%keyRemoved%"=="x%validshells%" ( REM sh 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 CALL %nextshell% "%~dp0%fname%" %* & SET task_exitcode=!errorlevel! ) ELSE ( ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells% SET task_exitcode=66 GOTO :exit ) ) ) @GOTO :endlib :getWslPath @SETLOCAL @SET "_path=%~p1" @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" @ENDLOCAL & ( @if "%~2" neq "" ( SET "%rtrn%=%result%" ) ELSE ( ECHO %result% ) ) @GOTO :eof :endlib : \ @REM @SET taskexit_code=!errorlevel! & goto :exit @GOTO :exit # } # rem call %nextshell% "%~dp0%~n0.cmd" %* # -*- 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 s2 set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup Hide :exit;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]/--]] } return [set ::punk::multishell::is_main($script_name)] } } } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload #puts "script : [info script]" #puts "argcount : $::argc" #puts "argvalues: $::argv" #puts "argv0 : $::argv0" # -- --- --- --- --- --- --- --- --- --- --- --- # # # -- --- --- --- --- --- --- --- --- --- --- --- # -- 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 # sh/bash \ shift && set -- "${@:1:$#-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 @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload #printf "start of bash or sh code" # # # -- --- --- --- --- --- --- --- # exitcode=0 ;#default assumption #-- 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 "tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 # # -- --- --- --- --- --- --- --- # # #printf "sh/bash done \n" # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload #------------------------------------------------------ fi exit ${exitcode} # end hide sh/bash 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 # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### function GetScriptName { $myInvocation.ScriptName } $scriptname = getScriptName # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"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 # -- --- --- --- # # # -- --- --- --- --- --- --- --- # tclsh $scriptname $args # # -- --- --- --- --- --- --- --- # # # unbal } # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload #"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' ' : end hide powershell-block from Tcl \ # This comment with closing brace should stay in place whether 'if' commented or not } : cmd exit label - return exitcode :exit : \ @REM @ECHO exitcode: !task_exitcode! : \ @EXIT /B !task_exitcode! # cmd has exited : end heredoc2 \ '@ <# # id:tailblock0 # -- powershell multiline comment #> <# # 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) #>