diff --git a/bin/getzig.cmd b/bin/getzig.cmd index 1108691d..2cb8900b 100644 --- a/bin/getzig.cmd +++ b/bin/getzig.cmd @@ -1,4 +1,4 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ @@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, zsh, bash, (sh diversion) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @rem ------------------------------------------------------------------------------------------------------------------------------- @@ -847,16 +847,6 @@ namespace eval ::punk::multishell { puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # -# -# - -# -# - - -# -# - # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -877,28 +867,61 @@ if {[::punk::multishell::is_main]} { HEREDOC1B_HIDE_FROM_BASH_AND_SH # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" -# echo "script: `echo $0 | sed 's/^-//'`" # use oldschool backticks and sed - lowest common denominator \ -# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` -# zsh diversion \ -# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi -# \ ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` # \ -echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" -# non-bash-like diversion \ -if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi -# sh/bash (or zsh?) \ -shift && set -- "${@:1:$((${#@}-1))}" -# \ -#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +echo "shell from ps: $ps_shellname" # \ echo "args: $@" +# ------------------------------------------------------------------------------ +# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # + +# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) +# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix +# posix compliant pop +pop() { + __pop_n=$(($1 - ${2:-1})) + if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then + POP_EXPR='set -- "${@:1:'$__pop_n'}"' + elif [ $__pop_n -ge 500 ]; then + POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" + else + __pop_index=0 + __pop_arguments="" + while [ $__pop_index -lt $__pop_n ]; do + __pop_index=$((__pop_index+1)) + __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" + done + POP_EXPR="set -- $__pop_arguments" + fi +} +# ------------------------------------------------------------------------------ + +# non-bash-like posix diversion \ +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 only needed if Tcl didn't exit or return above. +# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # + + +# zsh/bash \ +shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- sh/bash script section # -- leave as is if all that is required is launching the Tcl payload" @@ -910,7 +933,7 @@ if false==false # else { # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system -if [[ "$plat" = "Linux"* ]]; then +if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" @@ -922,11 +945,11 @@ elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" -elif [[ "$plat" = "MINGW32"* ]]; then +elif [[ "$plat" == "MINGW32"* ]]; then os="win32" -elif [[ "$plat" = "MINGW64"* ]]; then +elif [[ "$plat" == "MINGW64"* ]]; then os="win32" -elif [[ "$plat" = "CYGWIN_NT"* ]]; then +elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. @@ -972,34 +995,34 @@ for ln in "${arr_oslines[@]}"; do pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" - echo "nextshellpath: $nextshellpath" + # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" - echo "nextshelltype: $nextshelltype" + # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then - echo bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + 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 "sh/bash reporting exitcode: ${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 "bash payload" + #echo "zsh/bash payload" : fi -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -#printf "start of bash or sh code" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload +#printf "start of bash or zsh code" # #mkdir -p ./zig @@ -1020,29 +1043,9 @@ echo "Unimplemented: Download from ${full_uristring} and extract manually" # -# -# - -# -- --- --- --- --- --- --- --- -# -#-- 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 +#printf "zsh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} @@ -1078,7 +1081,6 @@ print "os $os\n"; # -- --- --- - my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; @@ -1088,21 +1090,11 @@ foreach my $a(@ARGV) { 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; @@ -1563,20 +1555,6 @@ if ($mirrors_response.StatusCode -eq 200) { # -# -# - - -# -- --- --- --- --- --- --- --- -# -#tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# -# -- --- --- --- --- --- --- --- - - -# -# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE diff --git a/bin/runtime.cmd b/bin/runtime.cmd index 13874262..be23cbb8 100755 --- a/bin/runtime.cmd +++ b/bin/runtime.cmd @@ -1,4 +1,4 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ @@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, zsh, bash, (sh diversion) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @rem ------------------------------------------------------------------------------------------------------------------------------- @@ -847,16 +847,6 @@ namespace eval ::punk::multishell { puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # -# -# - -# -# - - -# -# - # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -877,28 +867,61 @@ if {[::punk::multishell::is_main]} { HEREDOC1B_HIDE_FROM_BASH_AND_SH # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" -# echo "script: `echo $0 | sed 's/^-//'`" # use oldschool backticks and sed - lowest common denominator \ -# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` -# zsh diversion \ -# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi -# \ ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` # \ -echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" -# non-bash-like diversion \ -if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi -# sh/bash (or zsh?) \ -shift && set -- "${@:1:$((${#@}-1))}" -# \ -#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +echo "shell from ps: $ps_shellname" # \ echo "args: $@" +# ------------------------------------------------------------------------------ +# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # + +# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) +# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix +# posix compliant pop +pop() { + __pop_n=$(($1 - ${2:-1})) + if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then + POP_EXPR='set -- "${@:1:'$__pop_n'}"' + elif [ $__pop_n -ge 500 ]; then + POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" + else + __pop_index=0 + __pop_arguments="" + while [ $__pop_index -lt $__pop_n ]; do + __pop_index=$((__pop_index+1)) + __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" + done + POP_EXPR="set -- $__pop_arguments" + fi +} +# ------------------------------------------------------------------------------ + +# non-bash-like posix diversion \ +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 only needed if Tcl didn't exit or return above. +# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # + + +# zsh/bash \ +shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- sh/bash script section # -- leave as is if all that is required is launching the Tcl payload" @@ -910,7 +933,7 @@ if false==false # else { # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system -if [[ "$plat" = "Linux"* ]]; then +if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" @@ -922,11 +945,11 @@ elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" -elif [[ "$plat" = "MINGW32"* ]]; then +elif [[ "$plat" == "MINGW32"* ]]; then os="win32" -elif [[ "$plat" = "MINGW64"* ]]; then +elif [[ "$plat" == "MINGW64"* ]]; then os="win32" -elif [[ "$plat" = "CYGWIN_NT"* ]]; then +elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. @@ -972,34 +995,34 @@ for ln in "${arr_oslines[@]}"; do pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" - echo "nextshellpath: $nextshellpath" + # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" - echo "nextshelltype: $nextshelltype" + # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then - echo bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + 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 "sh/bash reporting exitcode: ${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 "bash payload" + #echo "zsh/bash payload" : fi -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -#printf "start of bash or sh code" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload +#printf "start of bash or zsh code" # @@ -1144,29 +1167,9 @@ esac # -# -# - -# -- --- --- --- --- --- --- --- -# -#-- 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 +#printf "zsh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} @@ -1202,7 +1205,6 @@ print "os $os\n"; # -- --- --- - my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; @@ -1212,21 +1214,11 @@ foreach my $a(@ARGV) { 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; @@ -1751,7 +1743,7 @@ function psmain { Write-host "-----------------------------------------------------------------------" Write-host "Runtimes for $arch" Write-host "Local $archfolder" - Write-host "Remote $$archurl" + Write-host "Remote $archurl" Write-host "-----------------------------------------------------------------------" Write-host "Local Remote" Write-host "-----------------------------------------------------------------------" @@ -1830,20 +1822,6 @@ exit 0 # -# -# - - -# -- --- --- --- --- --- --- --- -# -#tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# -# -- --- --- --- --- --- --- --- - - -# -# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE diff --git a/getpunk.cmd b/getpunk.cmd index e1bc3be5..858fd64f 100755 --- a/getpunk.cmd +++ b/getpunk.cmd @@ -1,4 +1,4 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ @@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, zsh, bash, (sh diversion) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @rem ------------------------------------------------------------------------------------------------------------------------------- @@ -266,7 +266,8 @@ set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n% set "param=%%L" @REM @echo ######### %%L @rem call :buildcmdline newcommandline param "{" "}" - call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell %= + @rem call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell ... -c %= + call :buildcmdline newcommandline param %= cmd.exe /c powershell ... -f %= @rem @echo . ) ) ELSE ( @@ -303,6 +304,7 @@ SETLOCAL EnableDelayedExpansion @IF "!selected_shelltype_trimmed!"=="pwsh" ( REM pwsh vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time REM test availability of preferred option of powershell7+ pwsh + REM when run without cmd.exe - pwsh will receive the semicolon (for cmd.exe unquoted semicolon and comma are separators that aren't seen in positional arguments) pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted 2>NUL; write-host "statusmessage: pwsh-found" >NUL SET pwshtest_exitcode=!errorlevel! REM ECHO pwshtest_exitcode !pwshtest_exitcode! @@ -310,18 +312,18 @@ SETLOCAL EnableDelayedExpansion IF !pwshtest_exitcode!==0 ( @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist% @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted - cmd /c pwsh -nop -nol -ExecutionPolicy bypass -c "%scriptrootname%.ps1" !newcommandline! + 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 -c "%scriptrootname%.ps1" !newcommandline! + 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 -c "%scriptrootname%.ps1" !newcommandline! + cmd /c powershell -nop -nol -ExecutionPolicy Bypass -f "%scriptrootname%.ps1" !newcommandline! SET task_exitcode=!errorlevel! ) ELSE ( IF "!selected_shelltype_trimmed!"=="wslbash" ( @@ -373,6 +375,7 @@ endlocal & set "%~3=%rtn%" exit /b %= ---------------------------------------------------------------------- =% +@REM padding :buildcmdline cmdlinevar paramvar wrapA wrapB %= quoting for cmd.exe /c pwsh -nop !args! =% @SETLOCAL EnableDelayedExpansion @@ -650,6 +653,8 @@ do if not defined param1 set %%~"param1=%2%%~" @REM boundary padding @REM boundary padding @REM boundary padding +@REM boundary padding +@REM boundary padding :stringTrimTrailingUnderscores @SETLOCAL @SET "rtrn=%~2" @@ -842,16 +847,6 @@ namespace eval ::punk::multishell { puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # -# -# - -# -# - - -# -# - # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -872,28 +867,61 @@ if {[::punk::multishell::is_main]} { HEREDOC1B_HIDE_FROM_BASH_AND_SH # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" -# echo "script: `echo $0 | sed 's/^-//'`" # use oldschool backticks and sed - lowest common denominator \ -# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` -# zsh diversion \ -# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi -# \ ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` # \ -echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" -# non-bash-like diversion \ -if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi -# sh/bash (or zsh?) \ -shift && set -- "${@:1:$((${#@}-1))}" -# \ -#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +echo "shell from ps: $ps_shellname" # \ echo "args: $@" +# ------------------------------------------------------------------------------ +# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # + +# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) +# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix +# posix compliant pop +pop() { + __pop_n=$(($1 - ${2:-1})) + if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then + POP_EXPR='set -- "${@:1:'$__pop_n'}"' + elif [ $__pop_n -ge 500 ]; then + POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" + else + __pop_index=0 + __pop_arguments="" + while [ $__pop_index -lt $__pop_n ]; do + __pop_index=$((__pop_index+1)) + __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" + done + POP_EXPR="set -- $__pop_arguments" + fi +} +# ------------------------------------------------------------------------------ + +# non-bash-like posix diversion \ +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 only needed if Tcl didn't exit or return above. +# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # + + +# zsh/bash \ +shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- sh/bash script section # -- leave as is if all that is required is launching the Tcl payload" @@ -905,7 +933,7 @@ if false==false # else { # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system -if [[ "$plat" = "Linux"* ]]; then +if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" @@ -917,11 +945,11 @@ elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" -elif [[ "$plat" = "MINGW32"* ]]; then +elif [[ "$plat" == "MINGW32"* ]]; then os="win32" -elif [[ "$plat" = "MINGW64"* ]]; then +elif [[ "$plat" == "MINGW64"* ]]; then os="win32" -elif [[ "$plat" = "CYGWIN_NT"* ]]; then +elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. @@ -967,34 +995,34 @@ for ln in "${arr_oslines[@]}"; do pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" - echo "nextshellpath: $nextshellpath" + # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" - echo "nextshelltype: $nextshelltype" + # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then - echo bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + 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 "sh/bash reporting exitcode: ${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 "bash payload" + #echo "zsh/bash payload" : fi -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -#printf "start of bash or sh code" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload +#printf "start of bash or zsh code" # @@ -1090,29 +1118,9 @@ 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 +#printf "zsh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} @@ -1148,7 +1156,6 @@ print "os $os\n"; # -- --- --- - my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; @@ -1158,21 +1165,11 @@ foreach my $a(@ARGV) { 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; @@ -1540,20 +1537,6 @@ write-host "`e[92m getpunk done `e[m" # -# -# - - -# -- --- --- --- --- --- --- --- -# -#tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# -# -- --- --- --- --- --- --- --- - - -# -# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE diff --git a/getpunk.cmd.lastrun b/getpunk.cmd.lastrun new file mode 100644 index 00000000..e1bc3be5 --- /dev/null +++ b/getpunk.cmd.lastrun @@ -0,0 +1,1596 @@ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +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 ------------------------------------------------------------------------------------------------------------------------------- +@rem return from endlocal macro - courtesy of jeb +@rem This allows return of values containing special characters from subroutines +@rem https://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal/8257951#8257951 +@rem ------------------------------------------------------------------------------------------------------------------------------- +@setlocal DisableDelayedExpansion +@echo off +%= 2 blank lines after next are required =% +set LF=^ + + +set ^"\n=^^^%LF%%LF%^%LF%%LF%^^" +%= I use EDE for EnableDelayeExpansion and DDE for DisableDelayedExpansion =% +set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n% + setlocal EnableDelayedExpansion%\n% + %= Take all variable names into the varName array =%%\n% + set varName_count=0%\n% + for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n% + %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n% + %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n =%%\n% + %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n% + %= The correct line will be detected by the (missing) enclosing quotes =%%\n% + set "retContent=1!LF!"%\n% + for /L %%n in (0 1 !varName_count!) do (%\n% + for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n% + set "content=!%%C!"%\n% + set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n% + if defined content (%\n% + %= This complex block is only for replacing '!' with '^!' =%%\n% + %= First replacing '"'->'""q' '^'->'^^' =%%\n% + set ^"content_EDE=!content:"=""q!"%\n% + set "content_EDE=!content_EDE:^=^^!"%\n% + %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n% + call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n% + %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!' =%%\n% + set "content_EDE=!content_EDE:""e=^!"%\n% + %= Now restore the quotes =%%\n% + set ^"content_EDE=!content_EDE:""q="!"%\n% + ) ELSE set "content_EDE="%\n% + set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n% + )%\n% + )%\n% + %= Now return all variables from retContent over the barrier =%%\n% + for /F "delims=" %%V in ("!retContent!") DO (%\n% + %= Only the first line can contain a single 1 =%%\n% + if "%%V"=="1" (%\n% + %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n% + endlocal%\n% + endlocal%\n% + ) ELSE (%\n% + %= This is true in EDE =%%\n% + if "!"=="" (%\n% + if %%V==%%~V (%\n% + %%V !%\n% + )%\n% + ) ELSE IF not %%V==%%~V (%\n% + %%~V%\n% + )%\n% + )%\n% + )%\n% + ) else set args=" + +@rem ------------------------------------------------------------------------------------------------------------------------------- +@SETLOCAL EnableExtensions EnableDelayedExpansion +@REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder +@REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) +@REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash if the script is called without an interpreter - but it may depend on the shell in use when called. +@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. +@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. +@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context +@SET "validshelltypes= pwsh____________ powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________ none____________" +@REM for batch - only win32 is relevant - but other scripts on other platforms also parse the nextshell block to determine next shell to launch +@REM nextshellpath and nextshelltype indices (underscore-padded to 16wide) are "other" plus those returned by Tcl platform pkg e.g win32,linux,freebsd,macosx +@REM The horrible underscore-padded fixed-widths are to keep the batch labels aligned whilst allowing values to be set +@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting +@REM Supporting more explicit oses than those listed may also require script padding adjustment +: <> +@SET "nextshellpath[win32___________]=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____________" +: <> +@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). +: <> +@SET "asadmin=0" +: <> +@SET "selected_shelltype=%nextshelltype[win32___________]%" +@REM @ECHO selected_shelltype %selected_shelltype% +@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed +@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed% +@SET "selected_shellpath=%nextshellpath[win32___________]%" +@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed +@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%" +@REM @ECHO keyremoved %keyRemoved% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix but should be left in place) +@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) +@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. +@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 +@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. +@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 +@REM ############################################################################################################################ +@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) +@REM -- Even something as simple as adding or removing an @REM +@REM -- From within punkshell - use: +@REM -- deck scriptwrap.checkfile filepath +@REM -- to check your templates or final wrapped scripts for byte boundary issues +@REM -- It will report any labels that are on boundaries +@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. +@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- Alternatively, as you should do anyway - test the final script on windows +@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. +@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. +@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label +@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. +@REM -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided +@REM ############################################################################################################################ +@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SET "winpath=%~dp0" %= e.g c:\punkshell\bin\ %= +@SET "fname=%~nx0" +@SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =% +@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 "{" "}" + call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell %= + @rem @echo . + ) +) ELSE ( + @for /F "delims=" %%L in (%paramfile%) do @( + SETLOCAL DisableDelayedExpansion + set "param=%%L" + call :buildcmdline newcommandline param + ) +) +@REM padding +SETLOCAL EnableDelayedExpansion + +@echo off +@IF EXIST %paramfile% ( + @DEL /F /Q %paramfile% +) +@IF EXIST %paramfile% ( + echo failed to delete %paramfile% + cat %paramfile% +) + + + +@REM @SET "squoted_args=" +@REM @for %%a in (%*) do @( +@REM set "v=%%a" +@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 + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted 2>NUL; write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + REM fallback to powershell if pwsh failed + IF !pwshtest_exitcode!==0 ( + @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist% + @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted + cmd /c pwsh -nop -nol -ExecutionPolicy bypass -c "%scriptrootname%.ps1" !newcommandline! + SET task_exitcode=!errorlevel! + ) ELSE ( + REM TODO prompt user with option to call script to install pwsh using winget + @rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% + cmd /c powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" !newcommandline! + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF "!selected_shelltype_trimmed!"=="powershell" ( + @rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% + cmd /c powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" !newcommandline! + 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 +%= ---------------------------------------------------------------------- =% + +: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 +: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 ": <>*" $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 ": <>*" $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" +# -- --- --- --- --- --- --- --- --- --- --- --- + +# +puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" +# + +# +# + +# +# + + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- 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 @: $@" +# echo "script: `echo $0 | sed 's/^-//'`" +# use oldschool backticks and sed - lowest common denominator \ +# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +# zsh diversion \ +# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi +# \ +ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +# \ +echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" +# non-bash-like diversion \ +if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi +# sh/bash (or zsh?) \ +shift && set -- "${@:1:$((${#@}-1))}" +# \ +#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +# \ +echo "args: $@" +# ------------------------------------------------------ +# -- 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. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### + +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 "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) +#awk tested on linux & freebsd +shellconfiglines=$( awk '/^:.*<>.*$/,/^:.*<>.*$/' "$0" | grep $os) +# echo $shellconfiglines; +# readarray requires bash 4.0 +if [[ "$ps_shellname" == "bash" ]]; then + readarray -t arr_oslines <<<"$shellconfiglines" +elif [[ "$ps_shellname" == "zsh" ]]; then + arr_oslines=("${(f)shellconfiglines}") +else + #fallback - doesn't seem to work in zsh - untested in early bash + IFS=$'\n' arr_oslines=($shellconfiglines) +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 bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + #e.g /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 + } +} +# Example usage: +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument, +# [Parameter(ValueFromRemainingArguments)] $opts +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargument' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# 'opts' { +# write-warning "Unused parameters: $($PSBoundParameters.$_)" +# } +# Default { +# write-warning "Unhandled parameter -> [$($_)]" +# } +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargument value is: $myargument" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +"Script Name : {0}" -f $scriptname | write-host +"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- +$thisfileContent = Get-Content $scriptname -Raw +$startTag = ": <>" +$endTag = ": <>" +$pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" +$match = [regex]::Match($thisfileContent,$pattern) +$asadmin = 0 +if ($match.Success) { + $admininfo = $match.Groups[1].Value + $asadmin = $admininfo.Contains("asadmin=1") + if ($asadmin) { + if (-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 = ": <>" +$endTag = ": <>" +$pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" +$match = [regex]::Match($thisfileContent,$pattern) +if ($match.Success) { + $plat = [System.Environment]::OSVersion.Platform + if ($plat -eq "Unix") { + $runtime_ident = [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier + switch ($runtime_ident.split("-")[0]) { + "freebsd" { + # untested + $os = "freebsd" + } + "linux" { + $os = "linux" + } + "osx" { + # osx-x64 or osx-arm64 ? + $os = "macosx" + } + default { + #openbsd, netbsd ? + $os = "other" + } + } + } else { + $os = "win32" + } + + $matchedlines = $match.Groups[1].Value + $nextshell_type = "" + $nextshell_path = "" + ForEach ($line in $($matchedlines -split "\r?\n")) { + $m = [regex]::Match($line,".*nextshelltype\[${os}[_]+\]=([^_]*)[_]*") + if ($m.Success) { + $nextshell_type = $m.Groups[1].Value + } + $m = [regex]::Match($line,".*nextshellpath\[${os}[_]+\]=([^_]*)[_]*") + if ($m.Success) { + $nextshell_path = $m.Groups[1].Value + } + if ($nextshell_type -ne "" -and $nextshell_path -ne "") { + break + } + } + if (-not (("pwsh", "powershell", "") -contains $nextshell_type)) { + #nextshell diversion exists for this platform + write-host "os: $os pwsh/powershell launching subshell of type: $nextshell_type shellpath: $nextshell_path on script $scriptname" + + # $arguments = @($($MyInvocation.MyCommand.Path)) + # $arguments += $args + # NOTE - this gives incorrect argument quoting e.g wrong number of arguments received by launched process for arguments: a "b c" + # $process = (Start-Process -FilePath $nextshell_path -ArgumentList $arguments -NoNewWindow -Wait) + # Exit $process.ExitCode + + & $nextshell_path $scriptname $args + exit $LASTEXITCODE + } +} + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload + +# + +#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) +#> + + + diff --git a/getpunk.ps1 b/getpunk.ps1 new file mode 100644 index 00000000..e1bc3be5 --- /dev/null +++ b/getpunk.ps1 @@ -0,0 +1,1596 @@ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +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 ------------------------------------------------------------------------------------------------------------------------------- +@rem return from endlocal macro - courtesy of jeb +@rem This allows return of values containing special characters from subroutines +@rem https://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal/8257951#8257951 +@rem ------------------------------------------------------------------------------------------------------------------------------- +@setlocal DisableDelayedExpansion +@echo off +%= 2 blank lines after next are required =% +set LF=^ + + +set ^"\n=^^^%LF%%LF%^%LF%%LF%^^" +%= I use EDE for EnableDelayeExpansion and DDE for DisableDelayedExpansion =% +set ^"endlocal=for %%# in (1 2) do if %%#==2 (%\n% + setlocal EnableDelayedExpansion%\n% + %= Take all variable names into the varName array =%%\n% + set varName_count=0%\n% + for %%C in (!args!) do set "varName[!varName_count!]=%%~C" ^& set /a varName_count+=1%\n% + %= Build one variable with a list of set statements for each variable delimited by newlines =%%\n% + %= The lists looks like --> set result1=myContent\n"set result1=myContent1"\nset result2=content2\nset result2=content2\n =%%\n% + %= Each result exists two times, the first for the case returning to DDE, the second for EDE =%%\n% + %= The correct line will be detected by the (missing) enclosing quotes =%%\n% + set "retContent=1!LF!"%\n% + for /L %%n in (0 1 !varName_count!) do (%\n% + for /F "delims=" %%C in ("!varName[%%n]!") DO (%\n% + set "content=!%%C!"%\n% + set "retContent=!retContent!"set !varName[%%n]!=!content!"!LF!"%\n% + if defined content (%\n% + %= This complex block is only for replacing '!' with '^!' =%%\n% + %= First replacing '"'->'""q' '^'->'^^' =%%\n% + set ^"content_EDE=!content:"=""q!"%\n% + set "content_EDE=!content_EDE:^=^^!"%\n% + %= Now it's poosible to use CALL SET and replace '!'->'""e!' =%%\n% + call set "content_EDE=%%content_EDE:^!=""e^!%%"%\n% + %= Now it's possible to replace '""e' to '^', this is effectivly '!' -> '^!' =%%\n% + set "content_EDE=!content_EDE:""e=^!"%\n% + %= Now restore the quotes =%%\n% + set ^"content_EDE=!content_EDE:""q="!"%\n% + ) ELSE set "content_EDE="%\n% + set "retContent=!retContent!set "!varName[%%n]!=!content_EDE!"!LF!"%\n% + )%\n% + )%\n% + %= Now return all variables from retContent over the barrier =%%\n% + for /F "delims=" %%V in ("!retContent!") DO (%\n% + %= Only the first line can contain a single 1 =%%\n% + if "%%V"=="1" (%\n% + %= We need to call endlocal twice, as there is one more setlocal in the macro itself =%%\n% + endlocal%\n% + endlocal%\n% + ) ELSE (%\n% + %= This is true in EDE =%%\n% + if "!"=="" (%\n% + if %%V==%%~V (%\n% + %%V !%\n% + )%\n% + ) ELSE IF not %%V==%%~V (%\n% + %%~V%\n% + )%\n% + )%\n% + )%\n% + ) else set args=" + +@rem ------------------------------------------------------------------------------------------------------------------------------- +@SETLOCAL EnableExtensions EnableDelayedExpansion +@REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder +@REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) +@REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash if the script is called without an interpreter - but it may depend on the shell in use when called. +@REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. +@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. +@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context +@SET "validshelltypes= pwsh____________ powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________ none____________" +@REM for batch - only win32 is relevant - but other scripts on other platforms also parse the nextshell block to determine next shell to launch +@REM nextshellpath and nextshelltype indices (underscore-padded to 16wide) are "other" plus those returned by Tcl platform pkg e.g win32,linux,freebsd,macosx +@REM The horrible underscore-padded fixed-widths are to keep the batch labels aligned whilst allowing values to be set +@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting +@REM Supporting more explicit oses than those listed may also require script padding adjustment +: <> +@SET "nextshellpath[win32___________]=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____________" +: <> +@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). +: <> +@SET "asadmin=0" +: <> +@SET "selected_shelltype=%nextshelltype[win32___________]%" +@REM @ECHO selected_shelltype %selected_shelltype% +@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed +@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed% +@SET "selected_shellpath=%nextshellpath[win32___________]%" +@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed +@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%" +@REM @ECHO keyremoved %keyRemoved% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix but should be left in place) +@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) +@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. +@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 +@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. +@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 +@REM ############################################################################################################################ +@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) +@REM -- Even something as simple as adding or removing an @REM +@REM -- From within punkshell - use: +@REM -- deck scriptwrap.checkfile filepath +@REM -- to check your templates or final wrapped scripts for byte boundary issues +@REM -- It will report any labels that are on boundaries +@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. +@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- Alternatively, as you should do anyway - test the final script on windows +@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. +@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. +@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label +@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. +@REM -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided +@REM ############################################################################################################################ +@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SET "winpath=%~dp0" %= e.g c:\punkshell\bin\ %= +@SET "fname=%~nx0" +@SET "scriptrootname=%~dp0%~n0" %= e.g c:\punkshell\bin\runtime (full path without extension) unavailable after shift, so store it =% +@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 "{" "}" + call :buildcmdline newcommandline param ' ' %= cmd.exe /c powershell %= + @rem @echo . + ) +) ELSE ( + @for /F "delims=" %%L in (%paramfile%) do @( + SETLOCAL DisableDelayedExpansion + set "param=%%L" + call :buildcmdline newcommandline param + ) +) +@REM padding +SETLOCAL EnableDelayedExpansion + +@echo off +@IF EXIST %paramfile% ( + @DEL /F /Q %paramfile% +) +@IF EXIST %paramfile% ( + echo failed to delete %paramfile% + cat %paramfile% +) + + + +@REM @SET "squoted_args=" +@REM @for %%a in (%*) do @( +@REM set "v=%%a" +@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 + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted 2>NUL; write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + REM fallback to powershell if pwsh failed + IF !pwshtest_exitcode!==0 ( + @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%scriptrootname%.ps1" %arglist% + @rem pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted + cmd /c pwsh -nop -nol -ExecutionPolicy bypass -c "%scriptrootname%.ps1" !newcommandline! + SET task_exitcode=!errorlevel! + ) ELSE ( + REM TODO prompt user with option to call script to install pwsh using winget + @rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% + cmd /c powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" !newcommandline! + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF "!selected_shelltype_trimmed!"=="powershell" ( + @rem powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" %arglist% + cmd /c powershell -nop -nol -ExecutionPolicy Bypass -c "%scriptrootname%.ps1" !newcommandline! + 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 +%= ---------------------------------------------------------------------- =% + +: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 +: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 ": <>*" $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 ": <>*" $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" +# -- --- --- --- --- --- --- --- --- --- --- --- + +# +puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" +# + +# +# + +# +# + + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- 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 @: $@" +# echo "script: `echo $0 | sed 's/^-//'`" +# use oldschool backticks and sed - lowest common denominator \ +# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +# zsh diversion \ +# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi +# \ +ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +# \ +echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" +# non-bash-like diversion \ +if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi +# sh/bash (or zsh?) \ +shift && set -- "${@:1:$((${#@}-1))}" +# \ +#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +# \ +echo "args: $@" +# ------------------------------------------------------ +# -- 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. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### + +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 "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) +#awk tested on linux & freebsd +shellconfiglines=$( awk '/^:.*<>.*$/,/^:.*<>.*$/' "$0" | grep $os) +# echo $shellconfiglines; +# readarray requires bash 4.0 +if [[ "$ps_shellname" == "bash" ]]; then + readarray -t arr_oslines <<<"$shellconfiglines" +elif [[ "$ps_shellname" == "zsh" ]]; then + arr_oslines=("${(f)shellconfiglines}") +else + #fallback - doesn't seem to work in zsh - untested in early bash + IFS=$'\n' arr_oslines=($shellconfiglines) +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 bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + #e.g /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 + } +} +# Example usage: +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument, +# [Parameter(ValueFromRemainingArguments)] $opts +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargument' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# 'opts' { +# write-warning "Unused parameters: $($PSBoundParameters.$_)" +# } +# Default { +# write-warning "Unhandled parameter -> [$($_)]" +# } +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargument value is: $myargument" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +"Script Name : {0}" -f $scriptname | write-host +"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- +$thisfileContent = Get-Content $scriptname -Raw +$startTag = ": <>" +$endTag = ": <>" +$pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" +$match = [regex]::Match($thisfileContent,$pattern) +$asadmin = 0 +if ($match.Success) { + $admininfo = $match.Groups[1].Value + $asadmin = $admininfo.Contains("asadmin=1") + if ($asadmin) { + if (-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 = ": <>" +$endTag = ": <>" +$pattern = "(?s)`n$startTag[^`n]*`n(.*?)`n$endTag" +$match = [regex]::Match($thisfileContent,$pattern) +if ($match.Success) { + $plat = [System.Environment]::OSVersion.Platform + if ($plat -eq "Unix") { + $runtime_ident = [System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier + switch ($runtime_ident.split("-")[0]) { + "freebsd" { + # untested + $os = "freebsd" + } + "linux" { + $os = "linux" + } + "osx" { + # osx-x64 or osx-arm64 ? + $os = "macosx" + } + default { + #openbsd, netbsd ? + $os = "other" + } + } + } else { + $os = "win32" + } + + $matchedlines = $match.Groups[1].Value + $nextshell_type = "" + $nextshell_path = "" + ForEach ($line in $($matchedlines -split "\r?\n")) { + $m = [regex]::Match($line,".*nextshelltype\[${os}[_]+\]=([^_]*)[_]*") + if ($m.Success) { + $nextshell_type = $m.Groups[1].Value + } + $m = [regex]::Match($line,".*nextshellpath\[${os}[_]+\]=([^_]*)[_]*") + if ($m.Success) { + $nextshell_path = $m.Groups[1].Value + } + if ($nextshell_type -ne "" -and $nextshell_path -ne "") { + break + } + } + if (-not (("pwsh", "powershell", "") -contains $nextshell_type)) { + #nextshell diversion exists for this platform + write-host "os: $os pwsh/powershell launching subshell of type: $nextshell_type shellpath: $nextshell_path on script $scriptname" + + # $arguments = @($($MyInvocation.MyCommand.Path)) + # $arguments += $args + # NOTE - this gives incorrect argument quoting e.g wrong number of arguments received by launched process for arguments: a "b c" + # $process = (Start-Process -FilePath $nextshell_path -ArgumentList $arguments -NoNewWindow -Wait) + # Exit $process.ExitCode + + & $nextshell_path $scriptname $args + exit $LASTEXITCODE + } +} + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload + +# + +#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) +#> + + + diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd index 7bfca8d4..6642a26f 100644 --- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd @@ -1,4 +1,4 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ @@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, zsh, bash, (sh diversion) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @rem ------------------------------------------------------------------------------------------------------------------------------- @@ -847,16 +847,6 @@ namespace eval ::punk::multishell { puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # -# -# - -# -# - - -# -# - # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -877,28 +867,61 @@ if {[::punk::multishell::is_main]} { HEREDOC1B_HIDE_FROM_BASH_AND_SH # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" -# echo "script: `echo $0 | sed 's/^-//'`" # use oldschool backticks and sed - lowest common denominator \ -# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` -# zsh diversion \ -# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi -# \ ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` # \ -echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" -# non-bash-like diversion \ -if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi -# sh/bash (or zsh?) \ -shift && set -- "${@:1:$((${#@}-1))}" -# \ -#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +echo "shell from ps: $ps_shellname" # \ echo "args: $@" +# ------------------------------------------------------------------------------ +# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # + +# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) +# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix +# posix compliant pop +pop() { + __pop_n=$(($1 - ${2:-1})) + if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then + POP_EXPR='set -- "${@:1:'$__pop_n'}"' + elif [ $__pop_n -ge 500 ]; then + POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" + else + __pop_index=0 + __pop_arguments="" + while [ $__pop_index -lt $__pop_n ]; do + __pop_index=$((__pop_index+1)) + __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" + done + POP_EXPR="set -- $__pop_arguments" + fi +} +# ------------------------------------------------------------------------------ + +# non-bash-like posix diversion \ +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 only needed if Tcl didn't exit or return above. +# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # + + +# zsh/bash \ +shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- sh/bash script section # -- leave as is if all that is required is launching the Tcl payload" @@ -910,7 +933,7 @@ if false==false # else { # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system -if [[ "$plat" = "Linux"* ]]; then +if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" @@ -922,11 +945,11 @@ elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" -elif [[ "$plat" = "MINGW32"* ]]; then +elif [[ "$plat" == "MINGW32"* ]]; then os="win32" -elif [[ "$plat" = "MINGW64"* ]]; then +elif [[ "$plat" == "MINGW64"* ]]; then os="win32" -elif [[ "$plat" = "CYGWIN_NT"* ]]; then +elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. @@ -972,62 +995,42 @@ for ln in "${arr_oslines[@]}"; do pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" - echo "nextshellpath: $nextshellpath" + # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" - echo "nextshelltype: $nextshelltype" + # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then - echo bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + 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 "sh/bash reporting exitcode: ${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 "bash payload" + #echo "zsh/bash payload" : fi -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -#printf "start of bash or sh code" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload +#printf "start of bash or zsh code" # echo "No bash code for this script. Try another program such as perl or tcl" >&2 # -# -# - -# -- --- --- --- --- --- --- --- -# -#-- 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 +#printf "zsh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} @@ -1063,7 +1066,6 @@ print "os $os\n"; # -- --- --- - my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; @@ -1073,21 +1075,11 @@ foreach my $a(@ARGV) { 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; @@ -1280,20 +1272,6 @@ Write-Error "No powershell code for this script. Try another program such as tcl "powershell args : {0}" -f ($args -join ", ") | write-host # -# -# - - -# -- --- --- --- --- --- --- --- -# -#tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# -# -- --- --- --- --- --- --- --- - - -# -# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd index 7bfca8d4..6642a26f 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd @@ -1,4 +1,4 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh zsh/bash cmd pwsh powershell" + "[rename set S;proc Hide shell_not_supported {proc $shell_not_supported args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ @@ -13,7 +13,7 @@ set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, (some sh) and/or powershelll (powershell.exe or pwsh.exe) +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, zsh, bash, (sh diversion) and/or powershelll (powershell.exe or pwsh.exe) @REM It should remain portable between unix-like OSes & windows if the proper structure is maintained. @REM ############################################################################################################################ @rem ------------------------------------------------------------------------------------------------------------------------------- @@ -847,16 +847,6 @@ namespace eval ::punk::multishell { puts stderr "No tcl code for this script. Try another program such as zsh or bash or perl" # -# -# - -# -# - - -# -# - # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -877,28 +867,61 @@ if {[::punk::multishell::is_main]} { HEREDOC1B_HIDE_FROM_BASH_AND_SH # Be wary of any non-trivial sed/awk etc - can be brittle to maintain across linux,freebsd,macosx due to differing implementations \ echo "var0: $0 @: $@" -# echo "script: `echo $0 | sed 's/^-//'`" # use oldschool backticks and sed - lowest common denominator \ -# echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` -# zsh diversion \ -# if [[ "$argv[*]" != "[*]" ]]; then /usr/bin/env bash "$0" "${argv[@]:2:$((${#argv[@]}-2))}"; exit $?; fi -# \ ps_shellname=`ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` # \ -echo "shell from ps: $ps_shellname argc: ${#@} inner: ${@:2:$((${#@}-2))}" -# non-bash-like diversion \ -if [[ "$ps_shellname" != "bash" && "$ps_shellname" != "zsh" ]]; then /usr/bin/env bash "$0" "${@:2:$((${#@}-2))}"; exit $?; fi -# sh/bash (or zsh?) \ -shift && set -- "${@:1:$((${#@}-1))}" -# \ -#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` +echo "shell from ps: $ps_shellname" # \ echo "args: $@" +# ------------------------------------------------------------------------------ +# -- This if block wraps posix sh diversion section - only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # + +# https://gist.github.com/fcard/e26c5a1f7c8b0674c17c7554fb0cd35c0 (MIT lic) +# https://stackoverflow.com/questions/63864755/remove-last-argument-in-shell-script-posix +# posix compliant pop +pop() { + __pop_n=$(($1 - ${2:-1})) + if [ -n "$ZSH_VERSION" -o -n "$BASH_VERSION" ]; then + POP_EXPR='set -- "${@:1:'$__pop_n'}"' + elif [ $__pop_n -ge 500 ]; then + POP_EXPR="set -- $(seq -s " " 1 $__pop_n | sed 's/[0-9]\+/"${\0}"/g')" + else + __pop_index=0 + __pop_arguments="" + while [ $__pop_index -lt $__pop_n ]; do + __pop_index=$((__pop_index+1)) + __pop_arguments="$__pop_arguments \"\${$__pop_index}\"" + done + POP_EXPR="set -- $__pop_arguments" + fi +} +# ------------------------------------------------------------------------------ + +# non-bash-like posix diversion \ +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 only needed if Tcl didn't exit or return above. +# -- This if block wraps whole zsh/bash and perl sections - only needed if Tcl didn't exit or return above. if false==false # else { then : # + + +# zsh/bash \ +shift && set -- "${@:1:$((${#@}-1))}" # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- sh/bash script section # -- leave as is if all that is required is launching the Tcl payload" @@ -910,7 +933,7 @@ if false==false # else { # ## ### ### ### ### ### ### ### ### ### ### ### ### ### plat=$(uname -s) #platform/system -if [[ "$plat" = "Linux"* ]]; then +if [[ "$plat" == "Linux"* ]]; then os="linux" elif [[ "$plat" == "Darwin"* ]]; then os="macosx" @@ -922,11 +945,11 @@ elif [[ "$plat" == "NetBSD"* ]]; then os="netbsd" elif [[ "$plat" == "OpenBSD"* ]]; then os="openbsd" -elif [[ "$plat" = "MINGW32"* ]]; then +elif [[ "$plat" == "MINGW32"* ]]; then os="win32" -elif [[ "$plat" = "MINGW64"* ]]; then +elif [[ "$plat" == "MINGW64"* ]]; then os="win32" -elif [[ "$plat" = "CYGWIN_NT"* ]]; then +elif [[ "$plat" == "CYGWIN_NT"* ]]; then os="win32" elif [[ "$plat" == "MSYS_NT"* ]]; then #review.. @@ -972,62 +995,42 @@ for ln in "${arr_oslines[@]}"; do pathraw="${splitln%%\"*}" #take everything before the quote - use %% to get longest match #remove trailing underscores (% means must match at end) nextshellpath="${pathraw/%_*/}" - echo "nextshellpath: $nextshellpath" + # echo "nextshellpath: $nextshellpath" elif [[ "$ln" == *"nextshelltype"* ]]; then splitln="${ln#*=}" typeraw="${splitln%%\"*}" nextshelltype="${typeraw/%_*/}" - echo "nextshelltype: $nextshelltype" + # echo "nextshelltype: $nextshelltype" fi done exitcode=0 #-- sh/bash launches nextscript here instead of shebang line at top if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then - echo bash launching subshell of type: $nextshelltype shellpath: $nextshellpath on "$0" with args "$@" + 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 "sh/bash reporting exitcode: ${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 "bash payload" + #echo "zsh/bash payload" : fi -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -#printf "start of bash or sh code" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin zsh Payload +#printf "start of bash or zsh code" # echo "No bash code for this script. Try another program such as perl or tcl" >&2 # -# -# - -# -- --- --- --- --- --- --- --- -# -#-- 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 +#printf "zsh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end zsh Payload #------------------------------------------------------ fi exit ${exitcode} @@ -1063,7 +1066,6 @@ print "os $os\n"; # -- --- --- - my $i =1; foreach my $a(@ARGV) { print "Arg # $i: $a\n"; @@ -1073,21 +1075,11 @@ foreach my $a(@ARGV) { 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; @@ -1280,20 +1272,6 @@ Write-Error "No powershell code for this script. Try another program such as tcl "powershell args : {0}" -f ($args -join ", ") | write-host # -# -# - - -# -- --- --- --- --- --- --- --- -# -#tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# -# -- --- --- --- --- --- --- --- - - -# -# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload Exit $LASTEXITCODE