From ba5a381c079073d4728f709e4efff10e45e7eef4 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 21 Aug 2025 03:10:08 +1000 Subject: [PATCH] scriptwrap fixes --- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 315 +++++--- src/bootsupport/modules/punk/zip-0.1.1.tm | 2 +- .../mix/commandset/scriptwrap-999999.0a1.0.tm | 315 +++++--- .../utility/scriptappwrappers/multishell.cmd | 208 ++++-- .../utility/scriptappwrappers/multishell1.cmd | 346 +++++++-- .../utility/scriptappwrappers/multishell2.cmd | 240 ++----- .../utility/scriptappwrappers/multishell3.cmd | 680 ++++++++++++++++++ src/modules/punk/pdf-999999.0a1.0.tm | 1 + src/modules/punk/zip-999999.0a1.0.tm | 2 +- .../custom/_project/punk.basic/src/make.tcl | 101 +-- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 315 +++++--- .../src/bootsupport/modules/punk/zip-0.1.1.tm | 2 +- .../_project/punk.project-0.1/src/make.tcl | 101 +-- .../punk/mix/commandset/scriptwrap-0.1.0.tm | 315 +++++--- .../src/bootsupport/modules/punk/zip-0.1.1.tm | 2 +- .../_project/punk.shell-0.1/src/make.tcl | 101 +-- src/scriptapps/bits.pl | 33 + src/scriptapps/bits.ps1 | 9 + src/scriptapps/bits.sh | 1 + src/scriptapps/bits_wrap.toml | 30 + src/scriptapps/example_wrap.toml | 10 +- src/scriptapps/fetchruntime.bash | 50 ++ src/scriptapps/fetchruntime.ps1 | 25 +- src/scriptapps/fetchruntime.tcl | 42 ++ src/scriptapps/fetchruntime_old.ps1 | 22 + src/scriptapps/fetchruntime_wrap.toml | 21 + .../punk/mix/commandset/scriptwrap-0.1.0.tm | 315 +++++--- .../utility/scriptappwrappers/multishell.cmd | 208 ++++-- .../utility/scriptappwrappers/multishell1.cmd | 346 +++++++-- .../utility/scriptappwrappers/multishell2.cmd | 240 ++----- .../utility/scriptappwrappers/multishell3.cmd | 680 ++++++++++++++++++ .../_vfscommon.vfs/modules/punk/pdf-0.1.0.tm | 1 + .../_vfscommon.vfs/modules/punk/zip-0.1.1.tm | 2 +- 33 files changed, 3856 insertions(+), 1225 deletions(-) create mode 100644 src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd create mode 100644 src/scriptapps/bits.pl create mode 100644 src/scriptapps/bits.ps1 create mode 100644 src/scriptapps/bits.sh create mode 100644 src/scriptapps/bits_wrap.toml create mode 100644 src/scriptapps/fetchruntime.bash create mode 100644 src/scriptapps/fetchruntime.tcl create mode 100644 src/scriptapps/fetchruntime_old.ps1 create mode 100644 src/scriptapps/fetchruntime_wrap.toml create mode 100644 src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd diff --git a/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 4f1af2bc..ef3e2a2f 100644 --- a/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -53,6 +53,7 @@ package require punk::args package require punk::mix package require punk::mix::base package require punk::fileline +package require punk::ansi #*** !doctools @@ -78,6 +79,7 @@ namespace eval punk::mix::commandset::scriptwrap { #[list_begin definitions] namespace export {[a-z]*} + namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval fileline { namespace import ::punk::fileline::lib::* @@ -807,7 +809,7 @@ namespace eval punk::mix::commandset::scriptwrap { } return $result } - #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site + #specific filepath to just wrap one script at the xxx-payload site #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #set usage "" #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n @@ -830,6 +832,17 @@ namespace eval punk::mix::commandset::scriptwrap { if {[tomlish::dict::path::exists $tomldict {.application.template}]} { dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] } + + if {[tomlish::dict::path::exists $tomldict {.application.as_admin}]} { + set val [tomlish::dict::path::get $tomldict {.application.as_admin.value}] + if {$val && 1} { + dict set resultd as_admin 1 + } else { + dict set resultd as_admin 0 + } + } else { + dict set resultd as_admin 0 + } set scripts [list] if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] @@ -872,6 +885,60 @@ namespace eval punk::mix::commandset::scriptwrap { return $resultd } + proc _default_configd {} { + set configd [dict create] + dict set configd template "" + dict set configd as_admin 0 + dict set configd scripts [list] + dict set configd default_outputfile "" + dict set configd default_nextshellpath "" + dict set configd default_nextshelltype "none" + foreach os {win32 dragonflybsd freebsd netbsd linux macosx other} { + dict set configd $os outputfile "" + dict set configd $os nextshellpath "" + dict set configd $os nextshelltype "none" + } + return $configd + } + proc _get_nextshell_script {configd} { + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[other___________]=tcl_____________" + + #delimeters + #: <> + #: <> + set script "" + dict for {k v} $configd { + if {[llength $v] % 2 == 0 && [dict exists $v nextshelltype]} { + set os $k + set n [expr {16 - [string length $os]}] + set _os [string repeat _ $n] + set path [dict get $v nextshellpath] + set n [expr {64 - [string length $path]}] + set _path [string repeat _ $n] + set type [dict get $v nextshelltype] + set n [expr {16 - [string length $type]}] + set _type [string repeat _ $n] + append script "@SET \"nextshellpath\[$os$_os\]=$path$_path\"" \n + append script "@SET \"nextshelltype\[$os$_os\]=$type$_type\"" \n + } + } + set script [string trimright $script \n] + return $script + } + punk::args::define { @id -id ::punk::mix::commandset::scriptwrap::multishell @cmd -name punk::mix::commandset::scriptwrap::multishell\ @@ -914,22 +981,22 @@ namespace eval punk::mix::commandset::scriptwrap { -returnextra -type boolean -default 0 @values -minvalues 0 -maxvalues 0 } - #: - #@SET "nextshellpath[win32___________]=tclsh___________________________" + #: <> + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" #@SET "nextshelltype[win32___________]=tcl_____________" - #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[dragonflybsd____]=tcl_____________" - #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[freebsd_________]=tcl_____________" - #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[netbsd__________]=tcl_____________" - #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[linux___________]=tcl_____________" - #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[macosx__________]=tcl_____________" - #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[other___________]=tcl_____________" - #: + #: <> proc multishell {args} { set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] lassign [dict values $argd] leaders opts values received @@ -947,7 +1014,7 @@ namespace eval punk::mix::commandset::scriptwrap { set startdir [pwd] set allowed_extensions [list tcl ps1 sh bash pl] #TODO - distinct sections for sh vs bash? needs experiments.. - #for now we use shell-pre-launch-subprocess etc + #for now we use shell-payload etc #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] @@ -990,13 +1057,15 @@ namespace eval punk::mix::commandset::scriptwrap { } set list_input_files [list] + set has_config 0 set configd [dict create] if {$scriptset ne ""} { puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" #.toml file may or may not exist if {[file exists ${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + puts stdout "[a bold green]Loading configuration from $scriptdir/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1008,6 +1077,7 @@ namespace eval punk::mix::commandset::scriptwrap { return false } } else { + set configd [_default_configd] puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" puts stdout "Will look for the following scripts in $scriptdir" foreach e $allowed_extensions { @@ -1018,12 +1088,17 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptdir/$scriptset.$e } } + dict set configd scripts $list_input_files } } else { + set configd [_default_configd] #expect a single script if {[file exists $specified_path]} { lappend list_input_files $specified_path } + dict set configd scripts [lmap f $list_input_files {file tail $f}] + set ftail [file tail $specified_path] + dict set configd default_outputfile [file rootname $ftail] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1061,8 +1136,9 @@ namespace eval punk::mix::commandset::scriptwrap { if {$scriptset ne ""} { #.toml file may or may not exist if {[file exists $scriptroot/${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + puts stdout "[a green]Loading configuration from $scriptroot/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1084,6 +1160,8 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptroot/$scriptset.$e } } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } } else { #expect a single script @@ -1094,6 +1172,8 @@ namespace eval punk::mix::commandset::scriptwrap { } lappend list_input_files $scriptroot/$filepath_or_scriptset } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1111,7 +1191,7 @@ namespace eval punk::mix::commandset::scriptwrap { } #assertion - customwrapper_folder var exists - but might be empty - if {[dict exists $configd template]} { + if {[dict get $configd template] ne ""} { set templatename [dict get $configd template] } else { if {$opt_template eq "\uFFFF"} { @@ -1119,6 +1199,7 @@ namespace eval punk::mix::commandset::scriptwrap { } else { set templatename $opt_template } + dict set configd template $templatename } set templatename_root [file rootname [file tail $templatename]] @@ -1199,21 +1280,26 @@ namespace eval punk::mix::commandset::scriptwrap { #todo #output_file extension may also depend on the template being used.. and/or the _wrap.toml config - if {[dict size $configd]} { - package require platform - set thisplatform [string tolower [platform::identify]] - set ptype [lindex [split $thisplatform -] 0] - switch -- $ptype { - win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} - default { - set ptype other - } + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other } + } + if {$has_config} { set out [dict get $configd $ptype outputfile] - set output_file [file join $output_folder $out] + if {[string trim $out] ne ""} { + set output_file [file join $output_folder $out] + } else { + #can be empty for this os if configured that way in xxx_wrap.toml + set output_file "" + } } else { #no _wrap.toml file available - if {$::tcl_platform(platform) eq "windows"} { + if {$ptype eq "win32"} { set output_extension .cmd } else { set output_extension .sh @@ -1224,6 +1310,11 @@ namespace eval punk::mix::commandset::scriptwrap { set infile [lindex $list_input_files 0] set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] } + dict set configd $ptype outputfile [file tail $output_file] + } + if {$output_file eq ""} { + puts stderr "No output file configured for platform $ptype" + return } @@ -1236,7 +1327,7 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout "wrap_in_multishell: target file $output_file already exists. File size: [$objFile_existing chunklen] Line count: [$objFile_existing linecount]" if {!$opt_force} { if {$opt_askme} { - set answer [util::askuser "Do you want to overwrite $output_file? Y|N"] + set answer [util::askuser "Do you want to [a bold white Red]overwrite[a] [a bold red]$output_file[a]? Y|N"] if {[string tolower $answer] ne "y"} { puts stderr "aborting due to user response '$answer' (required Y or y to proceed) use -force 1 or -askme 0 to avoid prompts." $objFile_existing destroy @@ -1264,92 +1355,122 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout $ln } puts stdout "-----------------------------------------------\n" - #foreach ln $template_lines { - #} - - if {[llength $list_input_files] > 1} { - #todo - puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" - return false - } - - #todo - split template at each etc marker and build a dict of parts + #if {[llength $list_input_files] > 1} { + # #todo + # puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" + # return false + #} - #hack - process one input - set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] - fconfigure $fdscript -translation binary - set script_data [read $fdscript] - close $fdscript - puts stdout "Read [string length $script_data] bytes of template data.." - set script_lines [split $script_data \n] - puts stdout "Displaying first 3 lines of your script between dashed lines..." - puts stdout "-----------------------------------------------" - foreach ln [lrange $script_lines 0 3] { - puts stdout $ln - } - puts stdout "-----------------------------------------------\n" - puts stdout "Target for above script data is '$output_file'" - set script_ext [string trim [file extension $filepath] .] - set lang [dict get $extension_langs [string tolower $script_ext]] - puts stdout "Language of script being wrapped is $lang" - if {$opt_askme} { - set answer [util::askuser "Does this look correct? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." - return + set lang_data [dict create] + foreach filepath $list_input_files { + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] + set fdscript [open $filepath r] + fconfigure $fdscript -translation binary + set script_data [read $fdscript] + close $fdscript + puts stdout "Read [string length $script_data] bytes of template data for lang: $lang" + set script_lines [split $script_data \n] + dict set lang_data $lang $script_lines + puts stdout "Displaying first 3 lines of your script [file tail $filepath] between dashed lines..." + puts stdout "[a green]$filepath[a]" + puts stdout "-----------------------------------------------" + foreach ln [lrange $script_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + puts stdout "Target for script data is '$output_file'" + puts stdout "Language of script being wrapped is [a bold yellow]$lang[a]" + if {$opt_askme} { + set answer [util::askuser "Does this look correct? Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } } } - set start_idx 0 - set end_idx 0 - set line_idx 0 - set existing_payload [list] + set template_ranges [list] + set data_items [list] + set trange [list 0] + set line_idx -1 + set opentag "" foreach ln $template_lines { - - if {[string match "#<$lang-pre-launch-subprocess>*" $ln]} { - set start_idx $line_idx - } elseif {[string match "#*" $ln]} { - set end_idx $line_idx - break - } elseif {$start_idx > 0} { - if {$end_idx > 0} { - lappend existing_payload [string trim $ln] - } + incr line_idx + if {$opentag eq ""} { + if {[string match ": <>*" $ln]} { + set opentag asadmin + lset trange 1 $line_idx ;#include tag in template + lappend template_ranges $trange + set trange [list $line_idx] + set asadmin [dict get $configd as_admin] + set scr "@SET \"asadmin=$asadmin\"" + lappend data_items $scr + } elseif {[string match ": <>*" $ln]} { + set opentag nextshell + lset trange 1 $line_idx + lappend template_ranges $trange + set trange [list $line_idx] + set nextshell_script [_get_nextshell_script $configd] + #puts stderr "-------------------" + #puts stderr "$nextshell_script" + lappend data_items $nextshell_script + } elseif {[string match "#<*-payload>*" $ln]} { + regexp {#<(.*)-payload>.*} $ln _ lang + if {[dict exists $lang_data $lang]} { + set script_lines [dict get $lang_data $lang] + set opentag payload-$lang + lset trange 1 $line_idx + lappend template_ranges $trange + lappend data_items [join $script_lines \n] + } + } } else { - + switch -- [string range $opentag 0 6] { + asadmin { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + nextshe { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + payload { + set lang [string range $opentag 8 end] ;#payload-xxx + if {[string match "#*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + } } - incr line_idx } - if {($start_idx == 0) || ($end_idx == 0)} { - error "wrap_in_multishell was unable to find payload area in template marked with #<$lang-pre-launch-subprocess> and # on separate lines" + if {$opentag eq ""} { + lset trange 1 end + lappend template_ranges $trange + } else { + error "multishell - unable to find closing tag for '$opentag'" } - set existing_string [join $existing_payload \n] - if {[string length [string trim $existing_string]]} { - puts stdout "EXISTING <$lang-pre-launch-subprocess> PAYLOAD!!" - puts stdout "-----------------------------------------------\n" - puts stdout $existing_string - puts stdout "-----------------------------------------------\n" - error "wrap_in_multishell found existing payload for language $lang ... aborting." - #todo - allow overwrite only in files outside of punkshell distribution? - if 0 { - puts stderr "Found existing $lang payload.. overwrite?" - if {$opt_askme} { - set answer [util::askuser "Are you sure you want to replace the $lang payload shown above? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts." - return - } - } + set newscript "" + foreach trange $template_ranges item $data_items { + append newscript [join [lrange $template_lines {*}$trange] \n] + if {$item ne ""} { + append newscript \n $item \n + } else { + append newscript \n } } - set tpl_head_lines [lrange $template_lines 0 $start_idx] ;#include tag line - set tpl_tail_lines [lrange $template_lines $end_idx end] - set newscript [join $tpl_head_lines \n]\n[join $script_lines \n]\n[join $tpl_tail_lines \n] + + puts stdout "New script is [string length $newscript] bytes" puts stdout $newscript + set fdtarget [open $output_file w] fconfigure $fdtarget -translation binary puts -nonewline $fdtarget $newscript diff --git a/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/bootsupport/modules/punk/zip-0.1.1.tm index 87863f88..2ed4f1e4 100644 --- a/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -400,7 +400,7 @@ tcl::namespace::eval punk::zip { #todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile set zout [open $outfile_zip w] fconfigure $zout -encoding iso8859-1 -translation binary - chan copy $inzip $zout + chan copy $inzip $zout close $zout } close $inzip diff --git a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm index 254bab83..521c3772 100644 --- a/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm +++ b/src/modules/punk/mix/commandset/scriptwrap-999999.0a1.0.tm @@ -53,6 +53,7 @@ package require punk::args package require punk::mix package require punk::mix::base package require punk::fileline +package require punk::ansi #*** !doctools @@ -78,6 +79,7 @@ namespace eval punk::mix::commandset::scriptwrap { #[list_begin definitions] namespace export {[a-z]*} + namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval fileline { namespace import ::punk::fileline::lib::* @@ -807,7 +809,7 @@ namespace eval punk::mix::commandset::scriptwrap { } return $result } - #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site + #specific filepath to just wrap one script at the xxx-payload site #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #set usage "" #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n @@ -830,6 +832,17 @@ namespace eval punk::mix::commandset::scriptwrap { if {[tomlish::dict::path::exists $tomldict {.application.template}]} { dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] } + + if {[tomlish::dict::path::exists $tomldict {.application.as_admin}]} { + set val [tomlish::dict::path::get $tomldict {.application.as_admin.value}] + if {$val && 1} { + dict set resultd as_admin 1 + } else { + dict set resultd as_admin 0 + } + } else { + dict set resultd as_admin 0 + } set scripts [list] if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] @@ -872,6 +885,60 @@ namespace eval punk::mix::commandset::scriptwrap { return $resultd } + proc _default_configd {} { + set configd [dict create] + dict set configd template "" + dict set configd as_admin 0 + dict set configd scripts [list] + dict set configd default_outputfile "" + dict set configd default_nextshellpath "" + dict set configd default_nextshelltype "none" + foreach os {win32 dragonflybsd freebsd netbsd linux macosx other} { + dict set configd $os outputfile "" + dict set configd $os nextshellpath "" + dict set configd $os nextshelltype "none" + } + return $configd + } + proc _get_nextshell_script {configd} { + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[other___________]=tcl_____________" + + #delimeters + #: <> + #: <> + set script "" + dict for {k v} $configd { + if {[llength $v] % 2 == 0 && [dict exists $v nextshelltype]} { + set os $k + set n [expr {16 - [string length $os]}] + set _os [string repeat _ $n] + set path [dict get $v nextshellpath] + set n [expr {64 - [string length $path]}] + set _path [string repeat _ $n] + set type [dict get $v nextshelltype] + set n [expr {16 - [string length $type]}] + set _type [string repeat _ $n] + append script "@SET \"nextshellpath\[$os$_os\]=$path$_path\"" \n + append script "@SET \"nextshelltype\[$os$_os\]=$type$_type\"" \n + } + } + set script [string trimright $script \n] + return $script + } + punk::args::define { @id -id ::punk::mix::commandset::scriptwrap::multishell @cmd -name punk::mix::commandset::scriptwrap::multishell\ @@ -914,22 +981,22 @@ namespace eval punk::mix::commandset::scriptwrap { -returnextra -type boolean -default 0 @values -minvalues 0 -maxvalues 0 } - #: - #@SET "nextshellpath[win32___________]=tclsh___________________________" + #: <> + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" #@SET "nextshelltype[win32___________]=tcl_____________" - #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[dragonflybsd____]=tcl_____________" - #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[freebsd_________]=tcl_____________" - #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[netbsd__________]=tcl_____________" - #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[linux___________]=tcl_____________" - #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[macosx__________]=tcl_____________" - #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[other___________]=tcl_____________" - #: + #: <> proc multishell {args} { set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] lassign [dict values $argd] leaders opts values received @@ -947,7 +1014,7 @@ namespace eval punk::mix::commandset::scriptwrap { set startdir [pwd] set allowed_extensions [list tcl ps1 sh bash pl] #TODO - distinct sections for sh vs bash? needs experiments.. - #for now we use shell-pre-launch-subprocess etc + #for now we use shell-payload etc #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] @@ -990,13 +1057,15 @@ namespace eval punk::mix::commandset::scriptwrap { } set list_input_files [list] + set has_config 0 set configd [dict create] if {$scriptset ne ""} { puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" #.toml file may or may not exist if {[file exists ${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + puts stdout "[a bold green]Loading configuration from $scriptdir/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1008,6 +1077,7 @@ namespace eval punk::mix::commandset::scriptwrap { return false } } else { + set configd [_default_configd] puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" puts stdout "Will look for the following scripts in $scriptdir" foreach e $allowed_extensions { @@ -1018,12 +1088,17 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptdir/$scriptset.$e } } + dict set configd scripts $list_input_files } } else { + set configd [_default_configd] #expect a single script if {[file exists $specified_path]} { lappend list_input_files $specified_path } + dict set configd scripts [lmap f $list_input_files {file tail $f}] + set ftail [file tail $specified_path] + dict set configd default_outputfile [file rootname $ftail] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1061,8 +1136,9 @@ namespace eval punk::mix::commandset::scriptwrap { if {$scriptset ne ""} { #.toml file may or may not exist if {[file exists $scriptroot/${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + puts stdout "[a green]Loading configuration from $scriptroot/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1084,6 +1160,8 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptroot/$scriptset.$e } } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } } else { #expect a single script @@ -1094,6 +1172,8 @@ namespace eval punk::mix::commandset::scriptwrap { } lappend list_input_files $scriptroot/$filepath_or_scriptset } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1111,7 +1191,7 @@ namespace eval punk::mix::commandset::scriptwrap { } #assertion - customwrapper_folder var exists - but might be empty - if {[dict exists $configd template]} { + if {[dict get $configd template] ne ""} { set templatename [dict get $configd template] } else { if {$opt_template eq "\uFFFF"} { @@ -1119,6 +1199,7 @@ namespace eval punk::mix::commandset::scriptwrap { } else { set templatename $opt_template } + dict set configd template $templatename } set templatename_root [file rootname [file tail $templatename]] @@ -1199,21 +1280,26 @@ namespace eval punk::mix::commandset::scriptwrap { #todo #output_file extension may also depend on the template being used.. and/or the _wrap.toml config - if {[dict size $configd]} { - package require platform - set thisplatform [string tolower [platform::identify]] - set ptype [lindex [split $thisplatform -] 0] - switch -- $ptype { - win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} - default { - set ptype other - } + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other } + } + if {$has_config} { set out [dict get $configd $ptype outputfile] - set output_file [file join $output_folder $out] + if {[string trim $out] ne ""} { + set output_file [file join $output_folder $out] + } else { + #can be empty for this os if configured that way in xxx_wrap.toml + set output_file "" + } } else { #no _wrap.toml file available - if {$::tcl_platform(platform) eq "windows"} { + if {$ptype eq "win32"} { set output_extension .cmd } else { set output_extension .sh @@ -1224,6 +1310,11 @@ namespace eval punk::mix::commandset::scriptwrap { set infile [lindex $list_input_files 0] set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] } + dict set configd $ptype outputfile [file tail $output_file] + } + if {$output_file eq ""} { + puts stderr "No output file configured for platform $ptype" + return } @@ -1236,7 +1327,7 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout "wrap_in_multishell: target file $output_file already exists. File size: [$objFile_existing chunklen] Line count: [$objFile_existing linecount]" if {!$opt_force} { if {$opt_askme} { - set answer [util::askuser "Do you want to overwrite $output_file? Y|N"] + set answer [util::askuser "Do you want to [a bold white Red]overwrite[a] [a bold red]$output_file[a]? Y|N"] if {[string tolower $answer] ne "y"} { puts stderr "aborting due to user response '$answer' (required Y or y to proceed) use -force 1 or -askme 0 to avoid prompts." $objFile_existing destroy @@ -1264,92 +1355,122 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout $ln } puts stdout "-----------------------------------------------\n" - #foreach ln $template_lines { - #} - - if {[llength $list_input_files] > 1} { - #todo - puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" - return false - } - - #todo - split template at each etc marker and build a dict of parts + #if {[llength $list_input_files] > 1} { + # #todo + # puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" + # return false + #} - #hack - process one input - set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] - fconfigure $fdscript -translation binary - set script_data [read $fdscript] - close $fdscript - puts stdout "Read [string length $script_data] bytes of template data.." - set script_lines [split $script_data \n] - puts stdout "Displaying first 3 lines of your script between dashed lines..." - puts stdout "-----------------------------------------------" - foreach ln [lrange $script_lines 0 3] { - puts stdout $ln - } - puts stdout "-----------------------------------------------\n" - puts stdout "Target for above script data is '$output_file'" - set script_ext [string trim [file extension $filepath] .] - set lang [dict get $extension_langs [string tolower $script_ext]] - puts stdout "Language of script being wrapped is $lang" - if {$opt_askme} { - set answer [util::askuser "Does this look correct? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." - return + set lang_data [dict create] + foreach filepath $list_input_files { + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] + set fdscript [open $filepath r] + fconfigure $fdscript -translation binary + set script_data [read $fdscript] + close $fdscript + puts stdout "Read [string length $script_data] bytes of template data for lang: $lang" + set script_lines [split $script_data \n] + dict set lang_data $lang $script_lines + puts stdout "Displaying first 3 lines of your script [file tail $filepath] between dashed lines..." + puts stdout "[a green]$filepath[a]" + puts stdout "-----------------------------------------------" + foreach ln [lrange $script_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + puts stdout "Target for script data is '$output_file'" + puts stdout "Language of script being wrapped is [a bold yellow]$lang[a]" + if {$opt_askme} { + set answer [util::askuser "Does this look correct? Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } } } - set start_idx 0 - set end_idx 0 - set line_idx 0 - set existing_payload [list] + set template_ranges [list] + set data_items [list] + set trange [list 0] + set line_idx -1 + set opentag "" foreach ln $template_lines { - - if {[string match "#<$lang-pre-launch-subprocess>*" $ln]} { - set start_idx $line_idx - } elseif {[string match "#*" $ln]} { - set end_idx $line_idx - break - } elseif {$start_idx > 0} { - if {$end_idx > 0} { - lappend existing_payload [string trim $ln] - } + incr line_idx + if {$opentag eq ""} { + if {[string match ": <>*" $ln]} { + set opentag asadmin + lset trange 1 $line_idx ;#include tag in template + lappend template_ranges $trange + set trange [list $line_idx] + set asadmin [dict get $configd as_admin] + set scr "@SET \"asadmin=$asadmin\"" + lappend data_items $scr + } elseif {[string match ": <>*" $ln]} { + set opentag nextshell + lset trange 1 $line_idx + lappend template_ranges $trange + set trange [list $line_idx] + set nextshell_script [_get_nextshell_script $configd] + #puts stderr "-------------------" + #puts stderr "$nextshell_script" + lappend data_items $nextshell_script + } elseif {[string match "#<*-payload>*" $ln]} { + regexp {#<(.*)-payload>.*} $ln _ lang + if {[dict exists $lang_data $lang]} { + set script_lines [dict get $lang_data $lang] + set opentag payload-$lang + lset trange 1 $line_idx + lappend template_ranges $trange + lappend data_items [join $script_lines \n] + } + } } else { - + switch -- [string range $opentag 0 6] { + asadmin { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + nextshe { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + payload { + set lang [string range $opentag 8 end] ;#payload-xxx + if {[string match "#*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + } } - incr line_idx } - if {($start_idx == 0) || ($end_idx == 0)} { - error "wrap_in_multishell was unable to find payload area in template marked with #<$lang-pre-launch-subprocess> and # on separate lines" + if {$opentag eq ""} { + lset trange 1 end + lappend template_ranges $trange + } else { + error "multishell - unable to find closing tag for '$opentag'" } - set existing_string [join $existing_payload \n] - if {[string length [string trim $existing_string]]} { - puts stdout "EXISTING <$lang-pre-launch-subprocess> PAYLOAD!!" - puts stdout "-----------------------------------------------\n" - puts stdout $existing_string - puts stdout "-----------------------------------------------\n" - error "wrap_in_multishell found existing payload for language $lang ... aborting." - #todo - allow overwrite only in files outside of punkshell distribution? - if 0 { - puts stderr "Found existing $lang payload.. overwrite?" - if {$opt_askme} { - set answer [util::askuser "Are you sure you want to replace the $lang payload shown above? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts." - return - } - } + set newscript "" + foreach trange $template_ranges item $data_items { + append newscript [join [lrange $template_lines {*}$trange] \n] + if {$item ne ""} { + append newscript \n $item \n + } else { + append newscript \n } } - set tpl_head_lines [lrange $template_lines 0 $start_idx] ;#include tag line - set tpl_tail_lines [lrange $template_lines $end_idx end] - set newscript [join $tpl_head_lines \n]\n[join $script_lines \n]\n[join $tpl_tail_lines \n] + + puts stdout "New script is [string length $newscript] bytes" puts stdout $newscript + set fdtarget [open $output_file w] fconfigure $fdtarget -translation binary puts -nonewline $fdtarget $newscript diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd index 9daf7ebf..8fb75ca3 100644 --- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell.cmd @@ -1,5 +1,5 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ -set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ : "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + @@ -16,41 +16,41 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. -@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder -@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) -@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called. +@REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder +@REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) +@REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash if the script is called without an interpreter - but it may depend on the shell in use when called. @REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. @REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. @REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________" +@SET "validshelltypes= 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 32 chars needed for a target, it can still be done but overall script padding may need checking/adjusting +@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting @REM Supporting more explicit oses than those listed may also require script padding adjustment -: -@SET "nextshellpath[win32___________]=tclsh___________________________" +: <> +@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" @SET "nextshelltype[win32___________]=tcl_____________" -@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" +@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[dragonflybsd____]=tcl_____________" -@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[freebsd_________]=tcl_____________" -@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[netbsd__________]=tcl_____________" -@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[linux___________]=tcl_____________" -@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[macosx__________]=tcl_____________" -@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[other___________]=tcl_____________" -: +: <> @rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). -: +: <> @SET "asadmin=0" -: +: <> @REM @ECHO nextshelltype is %nextshelltype[win32___________]% @REM @SET "selected_shelltype=%nextshelltype[win32___________]%" @SET "selected_shelltype=%nextshelltype[win32___________]%" @@ -143,7 +143,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%" @ECHO Next >> "%vbsGetPrivileges%" @ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" -@ECHO Launching script in new windows due to administrator elevation +@ECHO Launching script in new window due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -175,8 +175,11 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$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%"=="powershell" ( - REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time +@IF "!selected_shelltype_trimmed!"=="none" ( + SET selected_shelltype_trimmed=powershell +) +@IF "!selected_shelltype_trimmed!"=="powershell" ( + 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; write-host "statusmessage: pwsh-found" >NUL SET pwshtest_exitcode=!errorlevel! @@ -192,7 +195,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' SET task_exitcode=!errorlevel! ) ) ELSE ( - IF "%selected_shelltype_trimmed%"=="wslbash" ( + IF "!selected_shelltype_trimmed!"=="wslbash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% @@ -203,6 +206,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode + @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!" %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call; ) ELSE ( ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes% @@ -383,14 +387,15 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET "rtrn=%~2" @SET "string=%~1" @SET "trimstring=%~1" - @REM trim up to 31 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:###=% + @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 "" ( @@ -439,7 +444,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' # -- e.g tclsh filename.cmd # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### -rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore +rename set ""; rename S set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore Hide :exit_multishell;Hide {<#};Hide '@ namespace eval ::punk::multishell { set last_script_root [file dirname [file normalize ${::argv0}/__]] @@ -473,6 +478,9 @@ namespace eval ::punk::multishell { #puts "argv0 : $::argv0" # -- --- --- --- --- --- --- --- --- --- --- --- +# +puts stderr "No tcl code for this script. Try another program such as perl or bash" +# # # @@ -502,8 +510,20 @@ if {[::punk::multishell::is_main]} { # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload # end hide from unix shells \ HEREDOC1B_HIDE_FROM_BASH_AND_SH +# csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ +echo "script: `echo $0 | sed 's/^-//'`" +# csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ +echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +#csh/tcsh diversion \ +test "$argv[*]" != "[*]" && ( /usr/bin/env bash $argv[*]; exit ) +#other non-bash diversion \ +test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && /usr/bin/env bash $0 +#review \ +test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && exit # sh/bash \ shift && set -- "${@:1:$#-1}" + +#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` #------------------------------------------------------ # -- This if block only needed if Tcl didn't exit or return above. if false==false # else { @@ -518,10 +538,80 @@ if false==false # else { # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload + +if [[ "$OSTYPE" == "linux"* ]]; then + os="linux" +elif [[ "$OSTYPE" == "darwin"* ]]; then + os="macosx" +elif [[ "$OSTYPE" == "freebsd"* ]]; then + os="freebsd" +elif [[ "$OSTYPE" == "dragonflybsd"* ]]; then + os="dragonflybsd" +elif [[ "$OSTYPE" == "netbsd"* ]]; then + os="netbsd" +elif [[ "$OSTYPE" == "win32" ]]; then + os="win32" +elif [[ "$OSTYPE" == "msys" ]]; then + echo MSYS + os="win32" + #review - need ps/sed/awk to determine shell? + interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` + #use 'command -v' (shell builtin preferred over external which) + shellpath=`command -v $interp` + shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname + #"c:/windows/system32/" is quite likely in the path ahead of msys,git etc. + #This breaks calls to various unix utils such as sed etc (wsl related?) + export PATH="$shellfolder${PATH:+:${PATH}}" +else + #os="$OSTYPE" + os="other" +fi +echo ostype: $OSTYPE +shellconfigline=$( sed -n "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) +#echo $shellconfigline; +if [[ $shellconfigline == *"nextshelltype"* ]]; then + echo "found config for os $os" + split1="${shellconfigline#*=}" #remove everything through the first '=' + #echo "split1: $split1" + pathraw="${split1%%\"*}" #take everything before the quote - use %% to get longest match + pathraw="${pathraw//\"/}" #remove quote + nextshellpath="${pathraw/%_*/}" #remove trailing underscores (% = must match at end) + #echo "nextshellpath: $nextshellpath" + split2="${split1#*=}" + #echo "split2: $split2" + split2="${split2//\"/}" + nextshelltype="${split2/%_*/}" + echo "nextshelltype: $nextshelltype" +else + echo "unable to find config for os $os" + echo "shellconfigline: $shellconfigline" + nextshellpath="" + nextshelltype="" +fi exitcode=0 +#-- sh/bash launches nextscript here instead of shebang line at top +if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then + #echo bash launching subshell of type $nextshelltype $nextshellpath on "$0" + #/usr/bin/env tclsh "$0" "$@" + ${nextshellpath} "$0" "$@" + + exitcode=$? + #echo "sh/bash reporting exitcode: ${exitcode}" + exit $exitcode + #-- override exitcode example + #exit 66 +else + #already in bash - don't launch another process or we would loop + #echo "bash payload" + : +fi +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload #printf "start of bash or sh code" +# +echo "No bash code for this script. Try another program such as perl or tcl" >&2 +# + # # @@ -531,8 +621,8 @@ exitcode=0 #-- 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=$? +#/usr/bin/env tclsh "$0" "$@" +#exitcode=$? #echo "sh/bash reporting tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 @@ -558,8 +648,18 @@ exit ${exitcode} # ## ### ### ### ### ### ### ### ### ### ### ### ### ### =cut #!/user/bin/perl -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload 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(); @@ -571,13 +671,15 @@ my $exit_code = 0; -my $scriptname = $0; -print "perl $scriptname\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"; +# + # # @@ -585,7 +687,7 @@ foreach my $a(@ARGV) { # -- --- --- --- --- --- --- --- # -$exit_code=system("tclsh", $scriptname, @ARGV); +#$exit_code=system("tclsh", $scriptname, @ARGV); #print "perl reporting tcl exitcode: $exit_code"; # # -- --- --- --- --- --- --- --- @@ -680,12 +782,34 @@ function GetDynamicParamDictionary { # } #} #psmain @args -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host #"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- +$startTag = ": <>" +$endTag = ": <>" +$fileContent = Get-Content $scriptname -Raw +$pattern = "(?s)$startTag(.*?)$endTag" +$matches = [regex]::Matches($fileContent,$pattern) +$admininfo = $matches[0].Groups[1].Value +$asadmin = 0 +if ($matches.count) { + $asadmin = $admininfo.Contains("asadmin=1") + if ($asadmin) { + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + # If not elevated, relaunch with elevated privileges + # -Wait e.g for starting a service or other operations which remainder of script may depend on + Start-Process -FilePath "pwsh.exe" -ArgumentList "-NoProfile -NoExit -ExecutionPolicy Bypass -File $($MyInvocation.MyCommand.Path)" -Wait -Verb RunAs + Exit # Exit the current non-elevated process + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload + +# +Write-Error "No powershell code for this script. Try another program such as perl, tcl or bash" +# # # @@ -693,7 +817,7 @@ function GetDynamicParamDictionary { # -- --- --- --- --- --- --- --- # -tclsh $scriptname $args +#tclsh $scriptname $args #"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host # # -- --- --- --- --- --- --- --- diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd index 17fe4c15..9daf7ebf 100644 --- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd @@ -1,41 +1,65 @@ -: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^ -set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; 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 @ECHO; Hide ); Hide (;Hide echo; Hide @REM]#not necessary but can help avoid errs in testing" +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ +: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + : << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. +: shebang line is not required on unix or windows and will reduce functionality and/or portability. +: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. +@GOTO :skip_perl_pod_start ^; +=begin excludeperl +: skip_perl_pod_start : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { -: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. 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. @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, sh and/or powershelll (powershell.exe or pwsh.exe) +@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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: pmix scriptwrap.multishell -outputfolder +@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder @REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) @REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called. @REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. +@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. +@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh'" -@SET "shells[10]=pwsh" -@SET "shells[11]=sh" -@set "shells[12]=bash" -@SET "shells[13]=tclsh" +@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________" +@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 32 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 "nextshell=13" +@SET "nextshellpath[win32___________]=tclsh___________________________" +@SET "nextshelltype[win32___________]=tcl_____________" +@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" +@SET "nextshelltype[dragonflybsd____]=tcl_____________" +@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[freebsd_________]=tcl_____________" +@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[netbsd__________]=tcl_____________" +@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[linux___________]=tcl_____________" +@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[macosx__________]=tcl_____________" +@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[other___________]=tcl_____________" : @rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). : @SET "asadmin=0" : -@REM nextshell set to index for validshells .eg 10 for pwsh -@REM @ECHO nextshell is %nextshell% -@SET "selected=!shells[%nextshell%]!" -@REM @ECHO selected %selected% -@CALL SET "keyRemoved=%%validshells:'!selected!'=%%" +@REM @ECHO nextshelltype is %nextshelltype[win32___________]% +@REM @SET "selected_shelltype=%nextshelltype[win32___________]%" +@SET "selected_shelltype=%nextshelltype[win32___________]%" +@REM @ECHO selected_shelltype %selected_shelltype% +@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed +@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed% +@SET "selected_shellpath=%nextshellpath[win32___________]%" +@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed +@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%" @REM @ECHO keyremoved %keyRemoved% @REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @@ -49,16 +73,16 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @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 -- pmix scriptwrap.checkfile +@REM -- deck scriptwrap.checkfile @REM -- to check your templates or final wrapped scripts for byte boundary issues @REM -- It will report any labels that are on boundaries @REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. -@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using pmix scriptwrap.checkfile is still recommended. +@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 pmix scriptwrap.checkfile doesn't check all such boundaries. +@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 @@ -89,22 +113,36 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) @SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" @SET arglist=%* -@IF "%1"=="PUNK-ELEVATED" ( +@SET "qstrippedargs=args%arglist%" +@SET "qstrippedargs=%qstrippedargs:"=%" +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( GOTO :gotPrivileges ) @IF !asadmin!==1 ( net file 1>NUL 2>NUL @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) ) +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding @GOTO skip_privileges :getPrivileges -@IF '%1'=='PUNK-ELEVATED' (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) +@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.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%" +@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" @ECHO Launching script in new windows due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -113,7 +151,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @REM setlocal & pushd . @PUSHD . @cd /d %~dp0 -@IF "%1"=="PUNK-ELEVATED" ( +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( @DEL "%vbsGetPrivileges%" 1>nul 2>nul @SET arglist=%arglist:~14% ) @@ -124,7 +162,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @if not exist "%~dp0%~n0.ps1" ( @SET need_ps1=1 ) ELSE ( - fc "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >nul || goto different + fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different @REM @ECHO "files same" @SET need_ps1=0 ) @@ -134,10 +172,10 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @SET need_ps1=1 :pscontinue @IF !need_ps1!==1 ( - COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL + 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 "!shells[%nextshell%]!"=="pwsh" ( +@IF "%selected_shelltype_trimmed%"=="powershell" ( REM pws 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; write-host "statusmessage: pwsh-found" >NUL @@ -145,7 +183,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe REM ECHO pwshtest_exitcode !pwshtest_exitcode! REM fallback to powershell if pwsh failed IF !pwshtest_exitcode!==0 ( - pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel! + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! ) ELSE ( REM CALL powershell -nop -nol -c write-host powershell-found REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* @@ -153,24 +192,31 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe SET task_exitcode=!errorlevel! ) ) ELSE ( - IF "!shells[%nextshell%]!"=="bash" ( + IF "%selected_shelltype_trimmed%"=="wslbash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" - !shells[%nextshell%]! "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel! + %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% + SET task_exitcode=!errorlevel! ) ELSE ( - REM probably tclsh or sh - IF NOT "x%keyRemoved%"=="x%validshells%" ( + 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 - !shells[%nextshell%]! "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! + REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode + %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call; ) ELSE ( - ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% + ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes% SET task_exitcode=66 + @REM boundary padding + @REM boundary padding + @REM boundary padding + @REM boundary padding GOTO :exit_multishell ) ) ) @REM batch file library functions +@REM boundary padding @GOTO :endlib :getWslPath @@ -179,7 +225,9 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" - @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" + @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%" @@ -227,6 +275,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) @EXIT /B @REM boundary padding +@REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @@ -245,6 +294,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' @REM boundary padding @REM boundary padding +@REM boundary padding +@REM boundary padding @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @@ -289,7 +340,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) ) @EXIT /B - +@REM boundary padding +@REM boundary padding :stringToUpper @SETLOCAL @SET "rtrn=%~2" @@ -307,7 +359,47 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) ) @EXIT /B - +:stringToLower +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "retstring=%~1" + @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( + @SET "retstring=!retstring:%%A=%%A!" + ) + @SET "result=!retstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToLower %string% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:stringTrimTrailingUnderscores +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "trimstring=%~1" + @REM trim up to 31 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 "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" @@ -328,6 +420,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe :endlib : \ +@REM padding +@REM padding @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell @GOTO :exit_multishell # } @@ -348,9 +442,9 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore Hide :exit_multishell;Hide {<#};Hide '@ namespace eval ::punk::multishell { - set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script_root [file dirname [file normalize ${::argv0}/__]] set last_script [file dirname [file normalize [info script]/__]] - if {[info exists argv0] && + 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 @@ -365,7 +459,7 @@ namespace eval ::punk::multishell { 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}/__]]" + puts stderr "Info: script_root: [file dirname [file normalize ${::argv0}/__]]" return 0 } return [set ::punk::multishell::is_main($script_name)] @@ -380,10 +474,16 @@ namespace eval ::punk::multishell { # -- --- --- --- --- --- --- --- --- --- --- --- -# -# +# +# + +# +# +# +# + # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -414,33 +514,33 @@ if false==false # else { # -- leave as is if all that is required is launching the Tcl payload" # -- # -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default -# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate +# -- adjust the %nextshell% value above # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +exitcode=0 #printf "start of bash or sh code" -# -# +# +# # -- --- --- --- --- --- --- --- -# -exitcode=0 ;#default assumption +# #-- sh/bash launches Tcl here instead of shebang line at top #-- use exec to use exitcode (if any) directly from the tcl script #exec /usr/bin/env tclsh "$0" "$@" #-- alternative - can run sh/bash script after the tcl call. /usr/bin/env tclsh "$0" "$@" exitcode=$? -#echo "tcl exitcode: ${exitcode}" +#echo "sh/bash reporting tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 -# +# # -- --- --- --- --- --- --- --- -# -# +# +# #printf "sh/bash done \n" @@ -448,7 +548,57 @@ exitcode=$? #------------------------------------------------------ fi exit ${exitcode} -# end hide sh/bash block from Tcl +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- 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 +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload +my $exit_code = 0; +#use ExtUtils::Installed; +#my $installed = ExtUtils::Installed->new(); +#my @modules = $installed->modules(); +#print "Modules:\n"; +#foreach my $m (@modules) { +# print "$m\n"; +#} +# -- --- --- + + + +my $scriptname = $0; +print "perl $scriptname\n"; +my $i =1; +foreach my $a(@ARGV) { + print "Arg # $i: $a\n"; +} + +# +# + + + +# -- --- --- --- --- --- --- --- +# +$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 @@ -460,9 +610,76 @@ if 0 { # -- 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 +$scriptname = GetScriptName +function GetDynamicParamDictionary { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + [string] $CommandName + ) + + begin { + # Get a list of params that should be ignored (they're common to all advanced functions) + $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + } + + process { + # Create the dictionary that this scriptblock will return: + $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Convert to object array and get rid of Common params: + (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | + Where-Object { $CommonParameterNames -notcontains $_.Key } | + ForEach-Object { + $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( + $_.Key, + $_.Value.ParameterType, + $_.Value.Attributes + ) + $DynParamDictionary.Add($_.Key, $DynamicParameter) + } + + # Return the dynamic parameters + return $DynParamDictionary + } +} +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargumentname' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# #... +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargumentname value is: $myargumentname" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host @@ -470,22 +687,22 @@ $scriptname = getScriptName #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- -# +# tclsh $scriptname $args -# +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host +# # -- --- --- --- --- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' @@ -498,7 +715,7 @@ $1 = @' : \ @REM @ECHO exitcode: !task_exitcode! : \ -@IF "%1"=="PUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) : \ @EXIT /B !task_exitcode! # cmd has exited @@ -509,6 +726,7 @@ $1 = @' # -- powershell multiline comment #> <# +no script engine should try to run me # id:tailblock1 #  diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd index a9688b6a..17fe4c15 100644 --- a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd @@ -1,34 +1,29 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ -set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' -: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ -: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ -: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + +: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^ +set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; 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 @ECHO; Hide ); Hide (;Hide echo; 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 \ : { +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. 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. @REM ############################################################################################################################ @REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, 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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder +@REM e.g from within a running punkshell: pmix scriptwrap.multishell -outputfolder @REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) @REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh 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. @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh' (^14^) 'perl'" +@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh'" @SET "shells[10]=pwsh" @SET "shells[11]=sh" @set "shells[12]=bash" @SET "shells[13]=tclsh" -@SET "shells[14]=perl" : @SET "nextshell=13" : @@ -54,16 +49,16 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) @REM -- Even something as simple as adding or removing an @REM @REM -- From within punkshell - use: -@REM -- deck scriptwrap.checkfile +@REM -- pmix scriptwrap.checkfile @REM -- to check your templates or final wrapped scripts for byte boundary issues @REM -- It will report any labels that are on boundaries @REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. -@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- 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 pmix 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 -- It is unknown what versions of cmd interpreters behave this way - and pmix 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 @@ -94,40 +89,22 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$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" ( +@IF "%1"=="PUNK-ELEVATED" ( GOTO :gotPrivileges ) @IF !asadmin!==1 ( net file 1>NUL 2>NUL @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) ) -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM @GOTO skip_privileges :getPrivileges -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) +@IF '%1'=='PUNK-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 UAC.ShellExecute "%~dp0%~n0.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%" @ECHO Launching script in new windows due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -136,7 +113,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM setlocal & pushd . @PUSHD . @cd /d %~dp0 -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( +@IF "%1"=="PUNK-ELEVATED" ( @DEL "%vbsGetPrivileges%" 1>nul 2>nul @SET arglist=%arglist:~14% ) @@ -147,7 +124,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @if not exist "%~dp0%~n0.ps1" ( @SET need_ps1=1 ) ELSE ( - fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different + fc "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >nul || goto different @REM @ECHO "files same" @SET need_ps1=0 ) @@ -157,7 +134,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET need_ps1=1 :pscontinue @IF !need_ps1!==1 ( - COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL + COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL ) @REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /? @IF "!shells[%nextshell%]!"=="pwsh" ( @@ -168,8 +145,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' REM ECHO pwshtest_exitcode !pwshtest_exitcode! REM fallback to powershell if pwsh failed IF !pwshtest_exitcode!==0 ( - pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% - SET task_exitcode=!errorlevel! + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( REM CALL powershell -nop -nol -c write-host powershell-found REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* @@ -180,26 +156,21 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' IF "!shells[%nextshell%]!"=="bash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" - !shells[%nextshell%]! "!wslpath!%fname%" %arglist% - SET task_exitcode=!errorlevel! + !shells[%nextshell%]! "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( REM probably tclsh or sh IF NOT "x%keyRemoved%"=="x%validshells%" ( 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 - !shells[%nextshell%]! "%~dp0%fname%" %arglist% - SET task_exitcode=!errorlevel! + !shells[%nextshell%]! "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% SET task_exitcode=66 - @REM boundary padding - @REM boundary padding GOTO :exit_multishell ) ) ) @REM batch file library functions -@REM boundary padding @GOTO :endlib :getWslPath @@ -256,7 +227,6 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) @EXIT /B @REM boundary padding -@REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @@ -275,8 +245,6 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' @REM boundary padding @REM boundary padding -@REM boundary padding -@REM boundary padding @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @@ -412,15 +380,9 @@ namespace eval ::punk::multishell { # -- --- --- --- --- --- --- --- --- --- --- --- -# -# - -# -# - +# +# -# -# # -- --- --- --- --- --- --- --- --- --- --- --- @@ -452,33 +414,33 @@ if false==false # else { # -- 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 +# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -exitcode=0 #printf "start of bash or sh code" -# -# +# +# # -- --- --- --- --- --- --- --- -# +# +exitcode=0 ;#default assumption #-- sh/bash launches Tcl here instead of shebang line at top #-- use exec to use exitcode (if any) directly from the tcl script #exec /usr/bin/env tclsh "$0" "$@" #-- alternative - can run sh/bash script after the tcl call. /usr/bin/env tclsh "$0" "$@" exitcode=$? -#echo "sh/bash reporting tcl exitcode: ${exitcode}" +#echo "tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 -# +# # -- --- --- --- --- --- --- --- -# -# +# +# #printf "sh/bash done \n" @@ -486,57 +448,7 @@ exitcode=$? #------------------------------------------------------ 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 -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload -my $exit_code = 0; -#use ExtUtils::Installed; -#my $installed = ExtUtils::Installed->new(); -#my @modules = $installed->modules(); -#print "Modules:\n"; -#foreach my $m (@modules) { -# print "$m\n"; -#} -# -- --- --- - - - -my $scriptname = $0; -print "perl $scriptname\n"; -my $i =1; -foreach my $a(@ARGV) { - print "Arg # $i: $a\n"; -} - -# -# - - - -# -- --- --- --- --- --- --- --- -# -$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 +# end hide sh/bash block from Tcl # This comment with closing brace should stay in place whether if commented or not } #------------------------------------------------------ # begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above @@ -548,76 +460,9 @@ if 0 { # -- Do not edit if current file is the .ps1 # -- Edit the corresponding .cmd and it will autocopy # -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above -# -- custom script should generally go below the begin_powershell_payload line # ## ### ### ### ### ### ### ### ### ### ### ### ### ### function GetScriptName { $myInvocation.ScriptName } -$scriptname = GetScriptName -function GetDynamicParamDictionary { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$true, Mandatory=$true)] - [string] $CommandName - ) - - begin { - # Get a list of params that should be ignored (they're common to all advanced functions) - $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | - Get-Member -MemberType Properties | - Select-Object -ExpandProperty Name - } - - process { - # Create the dictionary that this scriptblock will return: - $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary - - # Convert to object array and get rid of Common params: - (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | - Where-Object { $CommonParameterNames -notcontains $_.Key } | - ForEach-Object { - $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( - $_.Key, - $_.Value.ParameterType, - $_.Value.Attributes - ) - $DynParamDictionary.Add($_.Key, $DynamicParameter) - } - - # Return the dynamic parameters - return $DynParamDictionary - } -} -# GetDynamicParamDictionary -# - This can make it easier to share a single set of param definitions between functions -# - sample usage -#function ParameterDefinitions { -# param( -# [Parameter(Mandatory)][string] $myargument -# ) -#} -#function psmain { -# [CmdletBinding()] -# param() -# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } -# process { -# #called once with $PSBoundParameters dictionary -# #can be used to validate arguments, or set a simpler variable name for access -# switch ($PSBoundParameters.keys) { -# 'myargumentname' { -# Set-Variable -Name $_ -Value $PSBoundParameters."$_" -# } -# #... -# } -# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { -# #... -# } -# } -# end { -# #Main function logic -# Write-Host "myargumentname value is: $myargumentname" -# #myotherfunction @PSBoundParameters -# } -#} -#psmain @args +$scriptname = getScriptName # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host @@ -625,22 +470,22 @@ function GetDynamicParamDictionary { #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- -# +# tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# +# # -- --- --- --- --- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' @@ -653,7 +498,7 @@ $1 = @' : \ @REM @ECHO exitcode: !task_exitcode! : \ -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) +@IF "%1"=="PUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) : \ @EXIT /B !task_exitcode! # cmd has exited @@ -664,7 +509,6 @@ $1 = @' # -- powershell multiline comment #> <# -no script engine should try to run me # id:tailblock1 #  diff --git a/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd new file mode 100644 index 00000000..a9688b6a --- /dev/null +++ b/src/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd @@ -0,0 +1,680 @@ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ +: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + +: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. +: shebang line is not required on unix or windows and will reduce functionality and/or portability. +: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. +@GOTO :skip_perl_pod_start ^; +=begin excludeperl +: skip_perl_pod_start +: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ +: { +@REM ############################################################################################################################ +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, 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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder +@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) +@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh 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. +@SETLOCAL EnableExtensions EnableDelayedExpansion +@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh' (^14^) 'perl'" +@SET "shells[10]=pwsh" +@SET "shells[11]=sh" +@set "shells[12]=bash" +@SET "shells[13]=tclsh" +@SET "shells[14]=perl" +: +@SET "nextshell=13" +: +@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). +: +@SET "asadmin=0" +: +@REM nextshell set to index for validshells .eg 10 for pwsh +@REM @ECHO nextshell is %nextshell% +@SET "selected=!shells[%nextshell%]!" +@REM @ECHO selected %selected% +@CALL SET "keyRemoved=%%validshells:'!selected!'=%%" +@REM @ECHO keyremoved %keyRemoved% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix but should be left in place) +@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) +@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. +@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 +@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. +@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 +@REM ############################################################################################################################ +@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) +@REM -- Even something as simple as adding or removing an @REM +@REM -- From within punkshell - use: +@REM -- deck scriptwrap.checkfile +@REM -- to check your templates or final wrapped scripts for byte boundary issues +@REM -- It will report any labels that are on boundaries +@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. +@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- Alternatively, as you should do anyway - test the final script on windows +@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. +@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. +@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label +@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. +@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided +@REM ############################################################################################################################ +@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SET "winpath=%~dp0" +@SET "fname=%~nx0" +@REM @ECHO fname %fname% +@REM @ECHO winpath %winpath% +@REM @ECHO commandlineascalled %0 +@REM @ECHO commandlineresolved %~f0 +@CALL :getNormalizedScriptTail nftail +@REM @ECHO normalizedscripttail %nftail% +@CALL :getFileTail %0 clinetail +@REM @ECHO clinetail %clinetail% +@CALL :stringToUpper %~nx0 capscripttail +@REM @ECHO capscriptname: %capscripttail% + +@IF "%nftail%"=="%capscripttail%" ( + @ECHO forcing asadmin=1 due to file name on filesystem being uppercase + @SET "asadmin=1" +) else ( + @CALL :stringToUpper %clinetail% capcmdlinetail + @REM @ECHO capcmdlinetail !capcmdlinetail! + IF "%clinetail%"=="!capcmdlinetail!" ( + @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase + @set "asadmin=1" + ) +) +@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" +@SET arglist=%* +@SET "qstrippedargs=args%arglist%" +@SET "qstrippedargs=%qstrippedargs:"=%" +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( + GOTO :gotPrivileges +) +@IF !asadmin!==1 ( + net file 1>NUL 2>NUL + @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) +) +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@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 windows 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 "!shells[%nextshell%]!"=="pwsh" ( + REM pws 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; write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + REM fallback to powershell if pwsh failed + IF !pwshtest_exitcode!==0 ( + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM CALL powershell -nop -nol -c write-host powershell-found + REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* + powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF "!shells[%nextshell%]!"=="bash" ( + CALL :getWslPath %winpath% wslpath + REM ECHO wslfullpath "!wslpath!%fname%" + !shells[%nextshell%]! "!wslpath!%fname%" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM probably tclsh or sh + IF NOT "x%keyRemoved%"=="x%validshells%" ( + 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 + !shells[%nextshell%]! "%~dp0%fname%" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% + SET task_exitcode=66 + @REM boundary padding + @REM boundary padding + GOTO :exit_multishell + ) + ) +) +@REM batch file library functions +@REM boundary padding +@GOTO :endlib + +:getWslPath +@SETLOCAL + @SET "_path=%~p1" + @SET "name=%~nx1" + @SET "drive=%~d1" + @SET "rtrn=%~2" + @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" +@ENDLOCAL & ( + @if "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + ECHO %result% + ) +) +@EXIT /B + +:getFileTail +@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd +@REM we can't use things such as %~nx1 as it can change capitalisation +@REM This function is designed explicitly to preserve capitalisation +@REM accepts full paths with either / or \ as delimiters - or +@SETLOCAL + @SET "rtrn=%~2" + @SET "arg=%~1" + @REM @SET "result=%_arg:*/=%" + @REM @SET "result=%~1" + @SET LF=^ + + + : The above 2 empty lines are important. Don't remove + @CALL :stringContains "!arg!" "\" hasBackSlash + @IF "!hasBackslash!"=="true" ( + @for %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @CALL :stringContains "!arg!" "/" hasForwardSlash + @IF "!hasForwardSlash!"=="true" ( + @FOR %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @set "result=%arg%" + ) + ) +@ENDLOCAL & ( + @if "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:getNormalizedScriptTail +@SETLOCAL + @SET "result=%~nx0" + @SET "rtrn=%~1" +@ENDLOCAL & ( + @IF "%~1" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B + +:getNormalizedFileTailFromPath +@REM warn via echo, and do not set return variable if path not found +@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' +@REM boundary padding +@REM boundary padding +@REM boundary padding +@REM boundary padding +@SETLOCAL + @CALL :stringContains %~1 "\" hasBackSlash + @CALL :stringContains %~1 "/" hasForwardSlash + @IF "%hasBackslash%-%hasForwardslash%"=="false-false" ( + @SET "P=%cd%%~1" + @CALL :getNormalizedFileTailFromPath "!P!" ftail2 + @SET "result=!ftail2!" + ) else ( + @IF EXIST "%~1" ( + @SET "result=%~nx1" + ) else ( + @ECHO error getNormalizedFileTailFromPath file not found: %~1 + @EXIT /B 1 + ) + ) + @SET "rtrn=%~2" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + @ECHO getNormalizedFileTailFromPath %1 result: %result% + ) +) +@EXIT /B + +:stringContains +@REM usage: @CALL:stringContains string needle returnvarname +@SETLOCAL + @SET "rtrn=%~3" + @SET "string=%~1" + @SET "needle=%~2" + @IF "!string:%needle%=!"=="!string!" @( + @SET "result=false" + ) ELSE ( + @SET "result=true" + ) +@ENDLOCAL & ( + @IF "%~3" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringContains %string% %needle% result: %result% + ) +) +@EXIT /B + +:stringToUpper +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "capstring=%~1" + @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @( + @SET "capstring=!capstring:%%A=%%A!" + ) + @SET "result=!capstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToUpper %string% result: %result% + ) +) +@EXIT /B + +: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 @SET taskexit_code=!errorlevel! & goto :exit_multishell +@GOTO :exit_multishell +# } +# -*- tcl -*- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- tcl script section +# -- This is a punk multishell file +# -- Primary payload target is Tcl, with sh,bash,powershell as helpers +# -- but it may equally be used with any of these being the primary script. +# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script +# -- i.e it is a polyglot file. +# -- The specific layout including some lines that appear just as comments is quite sensitive to change. +# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline. +# -- e.g ./filename.polypunk.cmd in sh or bash +# -- e.g tclsh filename.cmd +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore +Hide :exit_multishell;Hide {<#};Hide '@ +namespace eval ::punk::multishell { + set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script [file dirname [file normalize [info script]/__]] + if {[info exists argv0] && + $last_script eq $last_script_root + } { + set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode + } else { + set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. + } + if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { + proc ::punk::multishell::is_main {{script_name {}}} { + if {$script_name eq ""} { + set script_name [file dirname [file normalize [info script]/--]] + } + if {![info exists ::punk::multishell::is_main($script_name)]} { + #e.g a .dll or something else unanticipated + puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting" + puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]" + return 0 + } + return [set ::punk::multishell::is_main($script_name)] + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" +#puts "argv0 : $::argv0" +# -- --- --- --- --- --- --- --- --- --- --- --- + + +# +# + +# +# + + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- Best practice is to always return or exit above, or just by leaving the below defaults in place. +# -- If the multishell script is modified to have Tcl below the Tcl Payload section, +# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. +# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below +# -- but the sh/bash 'then' and 'fi' would also need to be uncommented. +# -- This facility left in place for experiments on whether configuration payloads etc can be appended +# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells +# -- can be made to ignore/cope with such data. +if {[::punk::multishell::is_main]} { + exit 0 +} else { + return +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +# end hide from unix shells \ +HEREDOC1B_HIDE_FROM_BASH_AND_SH +# sh/bash \ +shift && set -- "${@:1:$#-1}" +#------------------------------------------------------ +# -- This if block only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- sh/bash script section +# -- leave as is if all that is required is launching the Tcl payload" +# -- +# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default +# -- adjust the %nextshell% value above +# -- if sh/bash scripting needs to run on windows too. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +exitcode=0 +#printf "start of bash or sh code" + +# +# + +# -- --- --- --- --- --- --- --- +# +#-- 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 +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload +my $exit_code = 0; +#use ExtUtils::Installed; +#my $installed = ExtUtils::Installed->new(); +#my @modules = $installed->modules(); +#print "Modules:\n"; +#foreach my $m (@modules) { +# print "$m\n"; +#} +# -- --- --- + + + +my $scriptname = $0; +print "perl $scriptname\n"; +my $i =1; +foreach my $a(@ARGV) { + print "Arg # $i: $a\n"; +} + +# +# + + + +# -- --- --- --- --- --- --- --- +# +$exit_code=system("tclsh", $scriptname, @ARGV); +#print "perl reporting tcl exitcode: $exit_code"; +# +# -- --- --- --- --- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload +exit $exit_code; +__END__ + +# end hide sh/bash/perl block from Tcl +# This comment with closing brace should stay in place whether if commented or not } +#------------------------------------------------------ +# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above +if 0 { +: end heredoc1 - end hide from powershell \ +'@ +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- powershell/pwsh section +# -- Do not edit if current file is the .ps1 +# -- Edit the corresponding .cmd and it will autocopy +# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above +# -- custom script should generally go below the begin_powershell_payload line +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +function GetScriptName { $myInvocation.ScriptName } +$scriptname = GetScriptName +function GetDynamicParamDictionary { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + [string] $CommandName + ) + + begin { + # Get a list of params that should be ignored (they're common to all advanced functions) + $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + } + + process { + # Create the dictionary that this scriptblock will return: + $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Convert to object array and get rid of Common params: + (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | + Where-Object { $CommonParameterNames -notcontains $_.Key } | + ForEach-Object { + $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( + $_.Key, + $_.Value.ParameterType, + $_.Value.Attributes + ) + $DynParamDictionary.Add($_.Key, $DynamicParameter) + } + + # Return the dynamic parameters + return $DynParamDictionary + } +} +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargumentname' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# #... +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargumentname value is: $myargumentname" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +#"Script Name : {0}" -f $scriptname | write-host +#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +#"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- +# +tclsh $scriptname $args +#"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/pdf-999999.0a1.0.tm b/src/modules/punk/pdf-999999.0a1.0.tm index df74d26c..73ece0a9 100644 --- a/src/modules/punk/pdf-999999.0a1.0.tm +++ b/src/modules/punk/pdf-999999.0a1.0.tm @@ -1894,6 +1894,7 @@ tcl::namespace::eval punk::pdf::lib { dict set map %marker% "[punk::ansi::a bold cyan]MERGEDBLOCK[punk::ansi::a]" puts $outc [string map $map $opt_blocksep] } + set blockresult "" if {$opt_shrink_textfree_blocks} { set teststripped [punk::ansi::ansistrip $mergedblock] if {[string trim $teststripped] ne ""} { diff --git a/src/modules/punk/zip-999999.0a1.0.tm b/src/modules/punk/zip-999999.0a1.0.tm index 26b4737b..f4d7aaea 100644 --- a/src/modules/punk/zip-999999.0a1.0.tm +++ b/src/modules/punk/zip-999999.0a1.0.tm @@ -400,7 +400,7 @@ tcl::namespace::eval punk::zip { #todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile set zout [open $outfile_zip w] fconfigure $zout -encoding iso8859-1 -translation binary - chan copy $inzip $zout + chan copy $inzip $zout close $zout } close $inzip diff --git a/src/project_layouts/custom/_project/punk.basic/src/make.tcl b/src/project_layouts/custom/_project/punk.basic/src/make.tcl index c3cbc9aa..a9c40c35 100644 --- a/src/project_layouts/custom/_project/punk.basic/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.basic/src/make.tcl @@ -2839,56 +2839,77 @@ foreach vfstail $vfs_tails { #zipfs mkimg replaces the entire zipped vfs in the runtime - so we need the original data to be part of our targetvfs. puts stdout "building $vfsname.new with zipfs vfsdir:$vfstail cwd: [pwd]" file mkdir $targetvfs - set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + set raw_runtime $buildfolder/raw_$runtime_fullname - if {![file exists $rtmountpoint]} { - if {[catch { - tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname - } errM]} { - puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[info commands ::tcl::zipfs::mount] ne ""} { + + set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + + if {![file exists $rtmountpoint]} { if {[catch { - tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname } errM]} { - puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[catch { + tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + } errM]} { + puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + } } } - } - #strip any existing zipfs on the runtime.. - #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. - #which unfortunately Tcl does by default after the 2021 'fix' :( - #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 + #strip any existing zipfs on the runtime.. + #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. + #which unfortunately Tcl does by default after the 2021 'fix' :( + #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 - set raw_runtime $buildfolder/raw_$runtime_fullname - if {[file exists $rtmountpoint]} { - merge_over $rtmountpoint $targetvfs - #see if we can extract the exe part - set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] - if {$baseoffset != 0} { - #tcl was able to determine the compressed-data offset - #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' - set fdrt [open $building_runtime r] - chan configure $fdrt -translation binary - set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? - close $fdrt - set fdraw [open $raw_runtime w] - chan configure $fdraw -translation binary - puts -nonewline $fdraw $exedata - close $fdraw + if {[file exists $rtmountpoint]} { + merge_over $rtmountpoint $targetvfs + #see if we can extract the exe part + set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] + if {$baseoffset != 0} { + #tcl was able to determine the compressed-data offset + #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' + set fdrt [open $building_runtime r] + chan configure $fdrt -translation binary + set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? + close $fdrt + set fdraw [open $raw_runtime w] + chan configure $fdraw -translation binary + puts -nonewline $fdraw $exedata + close $fdraw + } else { + #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) + #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. + #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents + package require punk::zip + #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) + # - but for consistency we want raw_runtime to be emitted in the filesystem. + punk::zip::extract_preamble $building_runtime $raw_runtime + } } else { - #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) - #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. - #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents - package require punk::zip - #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) - # - but for consistency we want raw_runtime to be emitted in the filesystem. - punk::zip::extract_preamble $building_runtime $raw_runtime + #the input building_runtime wasn't mountable as a zip - so presumably a plain executable + #runtime executable possibly with kit/cookfs etc attached? + #If not - init.tcl probably won't be found? should we even proceed ?? + puts stderr "\x1b\[31mWARNING the runtime was not mountable as a zip - which means tcl_library was not extracted - the executable may not work with zipfs attached!\x1b\[m" + file copy -force $building_runtime $raw_runtime } } else { - #the input building_runtime wasn't mountable - so presumably a plain executable - #set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) - #set raw_runtime $buildfolder/raw_$runtime_fullname - file copy -force $building_runtime $raw_runtime + package require punk::zip + #tcl we are calling with doesn't have zipfs - can't mount + puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble" + set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip + set extractedzipfolder $buildfolder/extracted_$runtime_fullname + file delete $raw_runtime + file delete $extractedzipfile + file delete -force $extractedzipfolder + punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile + package require zipfile::decode + zipfile::decode::open $extractedzipfile + set archiveinfo [zipfile::decode::archive] + zipfile::decode::unzip $archiveinfo $extractedzipfolder + #todo - verify that init.tcl etc are present? + merge_over $extractedzipfolder $targetvfs } merge_over $sourcefolder/vfs/_vfscommon.vfs $targetvfs diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 4f1af2bc..ef3e2a2f 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -53,6 +53,7 @@ package require punk::args package require punk::mix package require punk::mix::base package require punk::fileline +package require punk::ansi #*** !doctools @@ -78,6 +79,7 @@ namespace eval punk::mix::commandset::scriptwrap { #[list_begin definitions] namespace export {[a-z]*} + namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval fileline { namespace import ::punk::fileline::lib::* @@ -807,7 +809,7 @@ namespace eval punk::mix::commandset::scriptwrap { } return $result } - #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site + #specific filepath to just wrap one script at the xxx-payload site #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #set usage "" #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n @@ -830,6 +832,17 @@ namespace eval punk::mix::commandset::scriptwrap { if {[tomlish::dict::path::exists $tomldict {.application.template}]} { dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] } + + if {[tomlish::dict::path::exists $tomldict {.application.as_admin}]} { + set val [tomlish::dict::path::get $tomldict {.application.as_admin.value}] + if {$val && 1} { + dict set resultd as_admin 1 + } else { + dict set resultd as_admin 0 + } + } else { + dict set resultd as_admin 0 + } set scripts [list] if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] @@ -872,6 +885,60 @@ namespace eval punk::mix::commandset::scriptwrap { return $resultd } + proc _default_configd {} { + set configd [dict create] + dict set configd template "" + dict set configd as_admin 0 + dict set configd scripts [list] + dict set configd default_outputfile "" + dict set configd default_nextshellpath "" + dict set configd default_nextshelltype "none" + foreach os {win32 dragonflybsd freebsd netbsd linux macosx other} { + dict set configd $os outputfile "" + dict set configd $os nextshellpath "" + dict set configd $os nextshelltype "none" + } + return $configd + } + proc _get_nextshell_script {configd} { + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[other___________]=tcl_____________" + + #delimeters + #: <> + #: <> + set script "" + dict for {k v} $configd { + if {[llength $v] % 2 == 0 && [dict exists $v nextshelltype]} { + set os $k + set n [expr {16 - [string length $os]}] + set _os [string repeat _ $n] + set path [dict get $v nextshellpath] + set n [expr {64 - [string length $path]}] + set _path [string repeat _ $n] + set type [dict get $v nextshelltype] + set n [expr {16 - [string length $type]}] + set _type [string repeat _ $n] + append script "@SET \"nextshellpath\[$os$_os\]=$path$_path\"" \n + append script "@SET \"nextshelltype\[$os$_os\]=$type$_type\"" \n + } + } + set script [string trimright $script \n] + return $script + } + punk::args::define { @id -id ::punk::mix::commandset::scriptwrap::multishell @cmd -name punk::mix::commandset::scriptwrap::multishell\ @@ -914,22 +981,22 @@ namespace eval punk::mix::commandset::scriptwrap { -returnextra -type boolean -default 0 @values -minvalues 0 -maxvalues 0 } - #: - #@SET "nextshellpath[win32___________]=tclsh___________________________" + #: <> + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" #@SET "nextshelltype[win32___________]=tcl_____________" - #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[dragonflybsd____]=tcl_____________" - #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[freebsd_________]=tcl_____________" - #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[netbsd__________]=tcl_____________" - #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[linux___________]=tcl_____________" - #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[macosx__________]=tcl_____________" - #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[other___________]=tcl_____________" - #: + #: <> proc multishell {args} { set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] lassign [dict values $argd] leaders opts values received @@ -947,7 +1014,7 @@ namespace eval punk::mix::commandset::scriptwrap { set startdir [pwd] set allowed_extensions [list tcl ps1 sh bash pl] #TODO - distinct sections for sh vs bash? needs experiments.. - #for now we use shell-pre-launch-subprocess etc + #for now we use shell-payload etc #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] @@ -990,13 +1057,15 @@ namespace eval punk::mix::commandset::scriptwrap { } set list_input_files [list] + set has_config 0 set configd [dict create] if {$scriptset ne ""} { puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" #.toml file may or may not exist if {[file exists ${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + puts stdout "[a bold green]Loading configuration from $scriptdir/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1008,6 +1077,7 @@ namespace eval punk::mix::commandset::scriptwrap { return false } } else { + set configd [_default_configd] puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" puts stdout "Will look for the following scripts in $scriptdir" foreach e $allowed_extensions { @@ -1018,12 +1088,17 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptdir/$scriptset.$e } } + dict set configd scripts $list_input_files } } else { + set configd [_default_configd] #expect a single script if {[file exists $specified_path]} { lappend list_input_files $specified_path } + dict set configd scripts [lmap f $list_input_files {file tail $f}] + set ftail [file tail $specified_path] + dict set configd default_outputfile [file rootname $ftail] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1061,8 +1136,9 @@ namespace eval punk::mix::commandset::scriptwrap { if {$scriptset ne ""} { #.toml file may or may not exist if {[file exists $scriptroot/${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + puts stdout "[a green]Loading configuration from $scriptroot/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1084,6 +1160,8 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptroot/$scriptset.$e } } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } } else { #expect a single script @@ -1094,6 +1172,8 @@ namespace eval punk::mix::commandset::scriptwrap { } lappend list_input_files $scriptroot/$filepath_or_scriptset } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1111,7 +1191,7 @@ namespace eval punk::mix::commandset::scriptwrap { } #assertion - customwrapper_folder var exists - but might be empty - if {[dict exists $configd template]} { + if {[dict get $configd template] ne ""} { set templatename [dict get $configd template] } else { if {$opt_template eq "\uFFFF"} { @@ -1119,6 +1199,7 @@ namespace eval punk::mix::commandset::scriptwrap { } else { set templatename $opt_template } + dict set configd template $templatename } set templatename_root [file rootname [file tail $templatename]] @@ -1199,21 +1280,26 @@ namespace eval punk::mix::commandset::scriptwrap { #todo #output_file extension may also depend on the template being used.. and/or the _wrap.toml config - if {[dict size $configd]} { - package require platform - set thisplatform [string tolower [platform::identify]] - set ptype [lindex [split $thisplatform -] 0] - switch -- $ptype { - win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} - default { - set ptype other - } + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other } + } + if {$has_config} { set out [dict get $configd $ptype outputfile] - set output_file [file join $output_folder $out] + if {[string trim $out] ne ""} { + set output_file [file join $output_folder $out] + } else { + #can be empty for this os if configured that way in xxx_wrap.toml + set output_file "" + } } else { #no _wrap.toml file available - if {$::tcl_platform(platform) eq "windows"} { + if {$ptype eq "win32"} { set output_extension .cmd } else { set output_extension .sh @@ -1224,6 +1310,11 @@ namespace eval punk::mix::commandset::scriptwrap { set infile [lindex $list_input_files 0] set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] } + dict set configd $ptype outputfile [file tail $output_file] + } + if {$output_file eq ""} { + puts stderr "No output file configured for platform $ptype" + return } @@ -1236,7 +1327,7 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout "wrap_in_multishell: target file $output_file already exists. File size: [$objFile_existing chunklen] Line count: [$objFile_existing linecount]" if {!$opt_force} { if {$opt_askme} { - set answer [util::askuser "Do you want to overwrite $output_file? Y|N"] + set answer [util::askuser "Do you want to [a bold white Red]overwrite[a] [a bold red]$output_file[a]? Y|N"] if {[string tolower $answer] ne "y"} { puts stderr "aborting due to user response '$answer' (required Y or y to proceed) use -force 1 or -askme 0 to avoid prompts." $objFile_existing destroy @@ -1264,92 +1355,122 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout $ln } puts stdout "-----------------------------------------------\n" - #foreach ln $template_lines { - #} - - if {[llength $list_input_files] > 1} { - #todo - puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" - return false - } - - #todo - split template at each etc marker and build a dict of parts + #if {[llength $list_input_files] > 1} { + # #todo + # puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" + # return false + #} - #hack - process one input - set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] - fconfigure $fdscript -translation binary - set script_data [read $fdscript] - close $fdscript - puts stdout "Read [string length $script_data] bytes of template data.." - set script_lines [split $script_data \n] - puts stdout "Displaying first 3 lines of your script between dashed lines..." - puts stdout "-----------------------------------------------" - foreach ln [lrange $script_lines 0 3] { - puts stdout $ln - } - puts stdout "-----------------------------------------------\n" - puts stdout "Target for above script data is '$output_file'" - set script_ext [string trim [file extension $filepath] .] - set lang [dict get $extension_langs [string tolower $script_ext]] - puts stdout "Language of script being wrapped is $lang" - if {$opt_askme} { - set answer [util::askuser "Does this look correct? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." - return + set lang_data [dict create] + foreach filepath $list_input_files { + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] + set fdscript [open $filepath r] + fconfigure $fdscript -translation binary + set script_data [read $fdscript] + close $fdscript + puts stdout "Read [string length $script_data] bytes of template data for lang: $lang" + set script_lines [split $script_data \n] + dict set lang_data $lang $script_lines + puts stdout "Displaying first 3 lines of your script [file tail $filepath] between dashed lines..." + puts stdout "[a green]$filepath[a]" + puts stdout "-----------------------------------------------" + foreach ln [lrange $script_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + puts stdout "Target for script data is '$output_file'" + puts stdout "Language of script being wrapped is [a bold yellow]$lang[a]" + if {$opt_askme} { + set answer [util::askuser "Does this look correct? Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } } } - set start_idx 0 - set end_idx 0 - set line_idx 0 - set existing_payload [list] + set template_ranges [list] + set data_items [list] + set trange [list 0] + set line_idx -1 + set opentag "" foreach ln $template_lines { - - if {[string match "#<$lang-pre-launch-subprocess>*" $ln]} { - set start_idx $line_idx - } elseif {[string match "#*" $ln]} { - set end_idx $line_idx - break - } elseif {$start_idx > 0} { - if {$end_idx > 0} { - lappend existing_payload [string trim $ln] - } + incr line_idx + if {$opentag eq ""} { + if {[string match ": <>*" $ln]} { + set opentag asadmin + lset trange 1 $line_idx ;#include tag in template + lappend template_ranges $trange + set trange [list $line_idx] + set asadmin [dict get $configd as_admin] + set scr "@SET \"asadmin=$asadmin\"" + lappend data_items $scr + } elseif {[string match ": <>*" $ln]} { + set opentag nextshell + lset trange 1 $line_idx + lappend template_ranges $trange + set trange [list $line_idx] + set nextshell_script [_get_nextshell_script $configd] + #puts stderr "-------------------" + #puts stderr "$nextshell_script" + lappend data_items $nextshell_script + } elseif {[string match "#<*-payload>*" $ln]} { + regexp {#<(.*)-payload>.*} $ln _ lang + if {[dict exists $lang_data $lang]} { + set script_lines [dict get $lang_data $lang] + set opentag payload-$lang + lset trange 1 $line_idx + lappend template_ranges $trange + lappend data_items [join $script_lines \n] + } + } } else { - + switch -- [string range $opentag 0 6] { + asadmin { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + nextshe { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + payload { + set lang [string range $opentag 8 end] ;#payload-xxx + if {[string match "#*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + } } - incr line_idx } - if {($start_idx == 0) || ($end_idx == 0)} { - error "wrap_in_multishell was unable to find payload area in template marked with #<$lang-pre-launch-subprocess> and # on separate lines" + if {$opentag eq ""} { + lset trange 1 end + lappend template_ranges $trange + } else { + error "multishell - unable to find closing tag for '$opentag'" } - set existing_string [join $existing_payload \n] - if {[string length [string trim $existing_string]]} { - puts stdout "EXISTING <$lang-pre-launch-subprocess> PAYLOAD!!" - puts stdout "-----------------------------------------------\n" - puts stdout $existing_string - puts stdout "-----------------------------------------------\n" - error "wrap_in_multishell found existing payload for language $lang ... aborting." - #todo - allow overwrite only in files outside of punkshell distribution? - if 0 { - puts stderr "Found existing $lang payload.. overwrite?" - if {$opt_askme} { - set answer [util::askuser "Are you sure you want to replace the $lang payload shown above? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts." - return - } - } + set newscript "" + foreach trange $template_ranges item $data_items { + append newscript [join [lrange $template_lines {*}$trange] \n] + if {$item ne ""} { + append newscript \n $item \n + } else { + append newscript \n } } - set tpl_head_lines [lrange $template_lines 0 $start_idx] ;#include tag line - set tpl_tail_lines [lrange $template_lines $end_idx end] - set newscript [join $tpl_head_lines \n]\n[join $script_lines \n]\n[join $tpl_tail_lines \n] + + puts stdout "New script is [string length $newscript] bytes" puts stdout $newscript + set fdtarget [open $output_file w] fconfigure $fdtarget -translation binary puts -nonewline $fdtarget $newscript diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm index 87863f88..2ed4f1e4 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -400,7 +400,7 @@ tcl::namespace::eval punk::zip { #todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile set zout [open $outfile_zip w] fconfigure $zout -encoding iso8859-1 -translation binary - chan copy $inzip $zout + chan copy $inzip $zout close $zout } close $inzip diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl index c3cbc9aa..a9c40c35 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl @@ -2839,56 +2839,77 @@ foreach vfstail $vfs_tails { #zipfs mkimg replaces the entire zipped vfs in the runtime - so we need the original data to be part of our targetvfs. puts stdout "building $vfsname.new with zipfs vfsdir:$vfstail cwd: [pwd]" file mkdir $targetvfs - set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + set raw_runtime $buildfolder/raw_$runtime_fullname - if {![file exists $rtmountpoint]} { - if {[catch { - tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname - } errM]} { - puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[info commands ::tcl::zipfs::mount] ne ""} { + + set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + + if {![file exists $rtmountpoint]} { if {[catch { - tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname } errM]} { - puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[catch { + tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + } errM]} { + puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + } } } - } - #strip any existing zipfs on the runtime.. - #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. - #which unfortunately Tcl does by default after the 2021 'fix' :( - #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 + #strip any existing zipfs on the runtime.. + #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. + #which unfortunately Tcl does by default after the 2021 'fix' :( + #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 - set raw_runtime $buildfolder/raw_$runtime_fullname - if {[file exists $rtmountpoint]} { - merge_over $rtmountpoint $targetvfs - #see if we can extract the exe part - set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] - if {$baseoffset != 0} { - #tcl was able to determine the compressed-data offset - #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' - set fdrt [open $building_runtime r] - chan configure $fdrt -translation binary - set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? - close $fdrt - set fdraw [open $raw_runtime w] - chan configure $fdraw -translation binary - puts -nonewline $fdraw $exedata - close $fdraw + if {[file exists $rtmountpoint]} { + merge_over $rtmountpoint $targetvfs + #see if we can extract the exe part + set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] + if {$baseoffset != 0} { + #tcl was able to determine the compressed-data offset + #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' + set fdrt [open $building_runtime r] + chan configure $fdrt -translation binary + set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? + close $fdrt + set fdraw [open $raw_runtime w] + chan configure $fdraw -translation binary + puts -nonewline $fdraw $exedata + close $fdraw + } else { + #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) + #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. + #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents + package require punk::zip + #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) + # - but for consistency we want raw_runtime to be emitted in the filesystem. + punk::zip::extract_preamble $building_runtime $raw_runtime + } } else { - #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) - #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. - #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents - package require punk::zip - #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) - # - but for consistency we want raw_runtime to be emitted in the filesystem. - punk::zip::extract_preamble $building_runtime $raw_runtime + #the input building_runtime wasn't mountable as a zip - so presumably a plain executable + #runtime executable possibly with kit/cookfs etc attached? + #If not - init.tcl probably won't be found? should we even proceed ?? + puts stderr "\x1b\[31mWARNING the runtime was not mountable as a zip - which means tcl_library was not extracted - the executable may not work with zipfs attached!\x1b\[m" + file copy -force $building_runtime $raw_runtime } } else { - #the input building_runtime wasn't mountable - so presumably a plain executable - #set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) - #set raw_runtime $buildfolder/raw_$runtime_fullname - file copy -force $building_runtime $raw_runtime + package require punk::zip + #tcl we are calling with doesn't have zipfs - can't mount + puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble" + set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip + set extractedzipfolder $buildfolder/extracted_$runtime_fullname + file delete $raw_runtime + file delete $extractedzipfile + file delete -force $extractedzipfolder + punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile + package require zipfile::decode + zipfile::decode::open $extractedzipfile + set archiveinfo [zipfile::decode::archive] + zipfile::decode::unzip $archiveinfo $extractedzipfolder + #todo - verify that init.tcl etc are present? + merge_over $extractedzipfolder $targetvfs } merge_over $sourcefolder/vfs/_vfscommon.vfs $targetvfs diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 4f1af2bc..ef3e2a2f 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -53,6 +53,7 @@ package require punk::args package require punk::mix package require punk::mix::base package require punk::fileline +package require punk::ansi #*** !doctools @@ -78,6 +79,7 @@ namespace eval punk::mix::commandset::scriptwrap { #[list_begin definitions] namespace export {[a-z]*} + namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval fileline { namespace import ::punk::fileline::lib::* @@ -807,7 +809,7 @@ namespace eval punk::mix::commandset::scriptwrap { } return $result } - #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site + #specific filepath to just wrap one script at the xxx-payload site #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #set usage "" #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n @@ -830,6 +832,17 @@ namespace eval punk::mix::commandset::scriptwrap { if {[tomlish::dict::path::exists $tomldict {.application.template}]} { dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] } + + if {[tomlish::dict::path::exists $tomldict {.application.as_admin}]} { + set val [tomlish::dict::path::get $tomldict {.application.as_admin.value}] + if {$val && 1} { + dict set resultd as_admin 1 + } else { + dict set resultd as_admin 0 + } + } else { + dict set resultd as_admin 0 + } set scripts [list] if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] @@ -872,6 +885,60 @@ namespace eval punk::mix::commandset::scriptwrap { return $resultd } + proc _default_configd {} { + set configd [dict create] + dict set configd template "" + dict set configd as_admin 0 + dict set configd scripts [list] + dict set configd default_outputfile "" + dict set configd default_nextshellpath "" + dict set configd default_nextshelltype "none" + foreach os {win32 dragonflybsd freebsd netbsd linux macosx other} { + dict set configd $os outputfile "" + dict set configd $os nextshellpath "" + dict set configd $os nextshelltype "none" + } + return $configd + } + proc _get_nextshell_script {configd} { + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[other___________]=tcl_____________" + + #delimeters + #: <> + #: <> + set script "" + dict for {k v} $configd { + if {[llength $v] % 2 == 0 && [dict exists $v nextshelltype]} { + set os $k + set n [expr {16 - [string length $os]}] + set _os [string repeat _ $n] + set path [dict get $v nextshellpath] + set n [expr {64 - [string length $path]}] + set _path [string repeat _ $n] + set type [dict get $v nextshelltype] + set n [expr {16 - [string length $type]}] + set _type [string repeat _ $n] + append script "@SET \"nextshellpath\[$os$_os\]=$path$_path\"" \n + append script "@SET \"nextshelltype\[$os$_os\]=$type$_type\"" \n + } + } + set script [string trimright $script \n] + return $script + } + punk::args::define { @id -id ::punk::mix::commandset::scriptwrap::multishell @cmd -name punk::mix::commandset::scriptwrap::multishell\ @@ -914,22 +981,22 @@ namespace eval punk::mix::commandset::scriptwrap { -returnextra -type boolean -default 0 @values -minvalues 0 -maxvalues 0 } - #: - #@SET "nextshellpath[win32___________]=tclsh___________________________" + #: <> + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" #@SET "nextshelltype[win32___________]=tcl_____________" - #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[dragonflybsd____]=tcl_____________" - #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[freebsd_________]=tcl_____________" - #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[netbsd__________]=tcl_____________" - #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[linux___________]=tcl_____________" - #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[macosx__________]=tcl_____________" - #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[other___________]=tcl_____________" - #: + #: <> proc multishell {args} { set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] lassign [dict values $argd] leaders opts values received @@ -947,7 +1014,7 @@ namespace eval punk::mix::commandset::scriptwrap { set startdir [pwd] set allowed_extensions [list tcl ps1 sh bash pl] #TODO - distinct sections for sh vs bash? needs experiments.. - #for now we use shell-pre-launch-subprocess etc + #for now we use shell-payload etc #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] @@ -990,13 +1057,15 @@ namespace eval punk::mix::commandset::scriptwrap { } set list_input_files [list] + set has_config 0 set configd [dict create] if {$scriptset ne ""} { puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" #.toml file may or may not exist if {[file exists ${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + puts stdout "[a bold green]Loading configuration from $scriptdir/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1008,6 +1077,7 @@ namespace eval punk::mix::commandset::scriptwrap { return false } } else { + set configd [_default_configd] puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" puts stdout "Will look for the following scripts in $scriptdir" foreach e $allowed_extensions { @@ -1018,12 +1088,17 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptdir/$scriptset.$e } } + dict set configd scripts $list_input_files } } else { + set configd [_default_configd] #expect a single script if {[file exists $specified_path]} { lappend list_input_files $specified_path } + dict set configd scripts [lmap f $list_input_files {file tail $f}] + set ftail [file tail $specified_path] + dict set configd default_outputfile [file rootname $ftail] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1061,8 +1136,9 @@ namespace eval punk::mix::commandset::scriptwrap { if {$scriptset ne ""} { #.toml file may or may not exist if {[file exists $scriptroot/${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + puts stdout "[a green]Loading configuration from $scriptroot/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1084,6 +1160,8 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptroot/$scriptset.$e } } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } } else { #expect a single script @@ -1094,6 +1172,8 @@ namespace eval punk::mix::commandset::scriptwrap { } lappend list_input_files $scriptroot/$filepath_or_scriptset } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1111,7 +1191,7 @@ namespace eval punk::mix::commandset::scriptwrap { } #assertion - customwrapper_folder var exists - but might be empty - if {[dict exists $configd template]} { + if {[dict get $configd template] ne ""} { set templatename [dict get $configd template] } else { if {$opt_template eq "\uFFFF"} { @@ -1119,6 +1199,7 @@ namespace eval punk::mix::commandset::scriptwrap { } else { set templatename $opt_template } + dict set configd template $templatename } set templatename_root [file rootname [file tail $templatename]] @@ -1199,21 +1280,26 @@ namespace eval punk::mix::commandset::scriptwrap { #todo #output_file extension may also depend on the template being used.. and/or the _wrap.toml config - if {[dict size $configd]} { - package require platform - set thisplatform [string tolower [platform::identify]] - set ptype [lindex [split $thisplatform -] 0] - switch -- $ptype { - win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} - default { - set ptype other - } + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other } + } + if {$has_config} { set out [dict get $configd $ptype outputfile] - set output_file [file join $output_folder $out] + if {[string trim $out] ne ""} { + set output_file [file join $output_folder $out] + } else { + #can be empty for this os if configured that way in xxx_wrap.toml + set output_file "" + } } else { #no _wrap.toml file available - if {$::tcl_platform(platform) eq "windows"} { + if {$ptype eq "win32"} { set output_extension .cmd } else { set output_extension .sh @@ -1224,6 +1310,11 @@ namespace eval punk::mix::commandset::scriptwrap { set infile [lindex $list_input_files 0] set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] } + dict set configd $ptype outputfile [file tail $output_file] + } + if {$output_file eq ""} { + puts stderr "No output file configured for platform $ptype" + return } @@ -1236,7 +1327,7 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout "wrap_in_multishell: target file $output_file already exists. File size: [$objFile_existing chunklen] Line count: [$objFile_existing linecount]" if {!$opt_force} { if {$opt_askme} { - set answer [util::askuser "Do you want to overwrite $output_file? Y|N"] + set answer [util::askuser "Do you want to [a bold white Red]overwrite[a] [a bold red]$output_file[a]? Y|N"] if {[string tolower $answer] ne "y"} { puts stderr "aborting due to user response '$answer' (required Y or y to proceed) use -force 1 or -askme 0 to avoid prompts." $objFile_existing destroy @@ -1264,92 +1355,122 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout $ln } puts stdout "-----------------------------------------------\n" - #foreach ln $template_lines { - #} - - if {[llength $list_input_files] > 1} { - #todo - puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" - return false - } - - #todo - split template at each etc marker and build a dict of parts + #if {[llength $list_input_files] > 1} { + # #todo + # puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" + # return false + #} - #hack - process one input - set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] - fconfigure $fdscript -translation binary - set script_data [read $fdscript] - close $fdscript - puts stdout "Read [string length $script_data] bytes of template data.." - set script_lines [split $script_data \n] - puts stdout "Displaying first 3 lines of your script between dashed lines..." - puts stdout "-----------------------------------------------" - foreach ln [lrange $script_lines 0 3] { - puts stdout $ln - } - puts stdout "-----------------------------------------------\n" - puts stdout "Target for above script data is '$output_file'" - set script_ext [string trim [file extension $filepath] .] - set lang [dict get $extension_langs [string tolower $script_ext]] - puts stdout "Language of script being wrapped is $lang" - if {$opt_askme} { - set answer [util::askuser "Does this look correct? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." - return + set lang_data [dict create] + foreach filepath $list_input_files { + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] + set fdscript [open $filepath r] + fconfigure $fdscript -translation binary + set script_data [read $fdscript] + close $fdscript + puts stdout "Read [string length $script_data] bytes of template data for lang: $lang" + set script_lines [split $script_data \n] + dict set lang_data $lang $script_lines + puts stdout "Displaying first 3 lines of your script [file tail $filepath] between dashed lines..." + puts stdout "[a green]$filepath[a]" + puts stdout "-----------------------------------------------" + foreach ln [lrange $script_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + puts stdout "Target for script data is '$output_file'" + puts stdout "Language of script being wrapped is [a bold yellow]$lang[a]" + if {$opt_askme} { + set answer [util::askuser "Does this look correct? Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } } } - set start_idx 0 - set end_idx 0 - set line_idx 0 - set existing_payload [list] + set template_ranges [list] + set data_items [list] + set trange [list 0] + set line_idx -1 + set opentag "" foreach ln $template_lines { - - if {[string match "#<$lang-pre-launch-subprocess>*" $ln]} { - set start_idx $line_idx - } elseif {[string match "#*" $ln]} { - set end_idx $line_idx - break - } elseif {$start_idx > 0} { - if {$end_idx > 0} { - lappend existing_payload [string trim $ln] - } + incr line_idx + if {$opentag eq ""} { + if {[string match ": <>*" $ln]} { + set opentag asadmin + lset trange 1 $line_idx ;#include tag in template + lappend template_ranges $trange + set trange [list $line_idx] + set asadmin [dict get $configd as_admin] + set scr "@SET \"asadmin=$asadmin\"" + lappend data_items $scr + } elseif {[string match ": <>*" $ln]} { + set opentag nextshell + lset trange 1 $line_idx + lappend template_ranges $trange + set trange [list $line_idx] + set nextshell_script [_get_nextshell_script $configd] + #puts stderr "-------------------" + #puts stderr "$nextshell_script" + lappend data_items $nextshell_script + } elseif {[string match "#<*-payload>*" $ln]} { + regexp {#<(.*)-payload>.*} $ln _ lang + if {[dict exists $lang_data $lang]} { + set script_lines [dict get $lang_data $lang] + set opentag payload-$lang + lset trange 1 $line_idx + lappend template_ranges $trange + lappend data_items [join $script_lines \n] + } + } } else { - + switch -- [string range $opentag 0 6] { + asadmin { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + nextshe { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + payload { + set lang [string range $opentag 8 end] ;#payload-xxx + if {[string match "#*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + } } - incr line_idx } - if {($start_idx == 0) || ($end_idx == 0)} { - error "wrap_in_multishell was unable to find payload area in template marked with #<$lang-pre-launch-subprocess> and # on separate lines" + if {$opentag eq ""} { + lset trange 1 end + lappend template_ranges $trange + } else { + error "multishell - unable to find closing tag for '$opentag'" } - set existing_string [join $existing_payload \n] - if {[string length [string trim $existing_string]]} { - puts stdout "EXISTING <$lang-pre-launch-subprocess> PAYLOAD!!" - puts stdout "-----------------------------------------------\n" - puts stdout $existing_string - puts stdout "-----------------------------------------------\n" - error "wrap_in_multishell found existing payload for language $lang ... aborting." - #todo - allow overwrite only in files outside of punkshell distribution? - if 0 { - puts stderr "Found existing $lang payload.. overwrite?" - if {$opt_askme} { - set answer [util::askuser "Are you sure you want to replace the $lang payload shown above? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts." - return - } - } + set newscript "" + foreach trange $template_ranges item $data_items { + append newscript [join [lrange $template_lines {*}$trange] \n] + if {$item ne ""} { + append newscript \n $item \n + } else { + append newscript \n } } - set tpl_head_lines [lrange $template_lines 0 $start_idx] ;#include tag line - set tpl_tail_lines [lrange $template_lines $end_idx end] - set newscript [join $tpl_head_lines \n]\n[join $script_lines \n]\n[join $tpl_tail_lines \n] + + puts stdout "New script is [string length $newscript] bytes" puts stdout $newscript + set fdtarget [open $output_file w] fconfigure $fdtarget -translation binary puts -nonewline $fdtarget $newscript diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm index 87863f88..2ed4f1e4 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/zip-0.1.1.tm @@ -400,7 +400,7 @@ tcl::namespace::eval punk::zip { #todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile set zout [open $outfile_zip w] fconfigure $zout -encoding iso8859-1 -translation binary - chan copy $inzip $zout + chan copy $inzip $zout close $zout } close $inzip diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl index c3cbc9aa..a9c40c35 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl @@ -2839,56 +2839,77 @@ foreach vfstail $vfs_tails { #zipfs mkimg replaces the entire zipped vfs in the runtime - so we need the original data to be part of our targetvfs. puts stdout "building $vfsname.new with zipfs vfsdir:$vfstail cwd: [pwd]" file mkdir $targetvfs - set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + set raw_runtime $buildfolder/raw_$runtime_fullname - if {![file exists $rtmountpoint]} { - if {[catch { - tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname - } errM]} { - puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[info commands ::tcl::zipfs::mount] ne ""} { + + set rtmountpoint //zipfs:/rtmounts/$runtime_fullname + + if {![file exists $rtmountpoint]} { if {[catch { - tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + tcl::zipfs::mount $building_runtime rtmounts/$runtime_fullname } errM]} { - puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + puts stderr "Failed to mount $building_runtime using standard api. Err:$errM\n trying reverse args on tcl::zipfs::mount..." + if {[catch { + tcl::zipfs::mount rtmounts/$runtime_fullname $building_runtime + } errM]} { + puts stderr "ALSO Failed to mount $building_runtime using reverse args to api. Err:$errM - no mountable zipfs on runtime?" + } } } - } - #strip any existing zipfs on the runtime.. - #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. - #which unfortunately Tcl does by default after the 2021 'fix' :( - #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 + #strip any existing zipfs on the runtime.. + #2024 - 'zipfs info //zipfs:/mountpoint' is supposed to give us the offset - but it doesn't if the exe has been 'adjusted' to use file offsets. + #which unfortunately Tcl does by default after the 2021 'fix' :( + #https://core.tcl-lang.org/tcl/tktview/aaa84fbbc5 - set raw_runtime $buildfolder/raw_$runtime_fullname - if {[file exists $rtmountpoint]} { - merge_over $rtmountpoint $targetvfs - #see if we can extract the exe part - set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] - if {$baseoffset != 0} { - #tcl was able to determine the compressed-data offset - #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' - set fdrt [open $building_runtime r] - chan configure $fdrt -translation binary - set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? - close $fdrt - set fdraw [open $raw_runtime w] - chan configure $fdraw -translation binary - puts -nonewline $fdraw $exedata - close $fdraw + if {[file exists $rtmountpoint]} { + merge_over $rtmountpoint $targetvfs + #see if we can extract the exe part + set baseoffset [lindex [tcl::zipfs::info $rtmountpoint] 3] + if {$baseoffset != 0} { + #tcl was able to determine the compressed-data offset + #either because runtime is a basic catted exe+zip, or Tcl fixed 'zipfs info' + set fdrt [open $building_runtime r] + chan configure $fdrt -translation binary + set exedata [read $fdrt $baseoffset] ;#may include stored password and ending header // REVIEW - strip it? + close $fdrt + set fdraw [open $raw_runtime w] + chan configure $fdraw -translation binary + puts -nonewline $fdraw $exedata + close $fdraw + } else { + #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) + #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. + #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents + package require punk::zip + #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) + # - but for consistency we want raw_runtime to be emitted in the filesystem. + punk::zip::extract_preamble $building_runtime $raw_runtime + } } else { - #presumably the supplied building_runtime has had its offsets adjusted so that it all appears within offsets off the zip. (file relative offsets) - #due to zipfs info bug - zipfs now can't tell us the offset of the compressed data. - #we need to use a similarly assumptive method as tclZipfs.c uses to determine the start of the compressed contents - package require punk::zip - #we don't technically need to extract the raw exe for 'zip' - as zipfs mkimg can work on the combined file (ignores zip) - # - but for consistency we want raw_runtime to be emitted in the filesystem. - punk::zip::extract_preamble $building_runtime $raw_runtime + #the input building_runtime wasn't mountable as a zip - so presumably a plain executable + #runtime executable possibly with kit/cookfs etc attached? + #If not - init.tcl probably won't be found? should we even proceed ?? + puts stderr "\x1b\[31mWARNING the runtime was not mountable as a zip - which means tcl_library was not extracted - the executable may not work with zipfs attached!\x1b\[m" + file copy -force $building_runtime $raw_runtime } } else { - #the input building_runtime wasn't mountable - so presumably a plain executable - #set building_runtime $buildfolder/build_$runtime_fullname ;#working copy of runtime executable - (possibly with kit/zipfs/cookfs etc attached!) - #set raw_runtime $buildfolder/raw_$runtime_fullname - file copy -force $building_runtime $raw_runtime + package require punk::zip + #tcl we are calling with doesn't have zipfs - can't mount + puts stderr "WARNING: tcl shell '[info nameofexecutable]' being used to build doesn't have zipfs - falling back to punk::zip::extract_preamble" + set extractedzipfile $buildfolder/extracted_$runtime_fullname.zip + set extractedzipfolder $buildfolder/extracted_$runtime_fullname + file delete $raw_runtime + file delete $extractedzipfile + file delete -force $extractedzipfolder + punk::zip::extract_preamble $building_runtime $raw_runtime $extractedzipfile + package require zipfile::decode + zipfile::decode::open $extractedzipfile + set archiveinfo [zipfile::decode::archive] + zipfile::decode::unzip $archiveinfo $extractedzipfolder + #todo - verify that init.tcl etc are present? + merge_over $extractedzipfolder $targetvfs } merge_over $sourcefolder/vfs/_vfscommon.vfs $targetvfs diff --git a/src/scriptapps/bits.pl b/src/scriptapps/bits.pl new file mode 100644 index 00000000..fbc0d603 --- /dev/null +++ b/src/scriptapps/bits.pl @@ -0,0 +1,33 @@ + +eval { + use Win32::Service; +}; +if (exists $INC{'Win32/Service.pm'}) { + print "Win32::Service is loaded\n"; +} else { + print "Win32::Service is not loaded\n"; + print "Attempting to install from cpan..\n"; + use CPAN; + my $module = 'Win32::Service'; + eval { + CPAN::Shell->install($module); + }; + if ($@) { + print "Error installing $module: $@"; + } else { + print "$module installed\n"; + } +} +eval { + use Win32::Service; +}; +if ($@) { + print "Unable to load or install Win32::Service. error: $@\n"; +} else { + my $service = "BITS"; + if (Win32::Service::StartService('', $service)) { + print "$service started\n"; + } else { + print "Failed to start $service\n"; + } +} \ No newline at end of file diff --git a/src/scriptapps/bits.ps1 b/src/scriptapps/bits.ps1 new file mode 100644 index 00000000..8acc22f2 --- /dev/null +++ b/src/scriptapps/bits.ps1 @@ -0,0 +1,9 @@ + +if ((Get-Service -name "BITS").Status -eq "Running") { + Write-Host "OK - BITS services is running" +} else { + Write-Host "WARNING - BITS service does not seem to be running" + Write-Host "Attempting to start.." + Restart-Service BITS + Write-Host "BITS service status is: " (Get-Service -name "BITS").Status +} diff --git a/src/scriptapps/bits.sh b/src/scriptapps/bits.sh new file mode 100644 index 00000000..1a778be6 --- /dev/null +++ b/src/scriptapps/bits.sh @@ -0,0 +1 @@ +echo "The bits script is only available for the windows platform where it is used to start the BITS service" diff --git a/src/scriptapps/bits_wrap.toml b/src/scriptapps/bits_wrap.toml new file mode 100644 index 00000000..0d14c958 --- /dev/null +++ b/src/scriptapps/bits_wrap.toml @@ -0,0 +1,30 @@ +[application] + template="punk.multishell.cmd" + as_admin=true + + #scripts=[ + # "example.sh", + # "example.tcl" + #] + scripts=[ + "bits.ps1", + "bits.sh", + "bits.pl" + ] + + default_outputfile="bits.sh" + default_nextshellpath="/usr/bin/env bash" + default_nextshelltype="bash" + + + #valid nextshelltype entries are: tcl perl powershell bash. + #nextshellpath entries must be 64 characters or less. + #win32.nextshellpath="perl" + #win32.nextshelltype="perl" + + #win32.nextshellpath="c:/program files/powershell/7/pwsh.exe" + win32.nextshellpath="pwsh" + win32.nextshelltype="powershell" + win32.outputfile="bits.cmd" + + diff --git a/src/scriptapps/example_wrap.toml b/src/scriptapps/example_wrap.toml index 3e7b0d62..5476485e 100644 --- a/src/scriptapps/example_wrap.toml +++ b/src/scriptapps/example_wrap.toml @@ -1,5 +1,6 @@ [application] template="punk.multishell.cmd" + as_admin=false #scripts=[ # "example.sh", @@ -15,11 +16,16 @@ #valid nextshelltype entries are: tcl perl powershell bash. - #nextshellpath entries must be 32 characters or less. + #nextshellpath entries must be 64 characters or less. + + # win32.nextshellpath="c:/program files/git/usr/bin/bash.exe" + # win32.nextshelltype="bash" + # win32.nextshellpath="c:/program files/powershell/7/pwsh.exe" + # win32.nextshelltype="powershell" win32.nextshellpath="tclsh" win32.nextshelltype="tcl" - win32.outputfile="example_out.bat" + win32.outputfile="example_out.cmd" dragonflybsd.nextshellpath="/usr/bin/env tclsh" dragonflybsd.nextshelltype="tcl" diff --git a/src/scriptapps/fetchruntime.bash b/src/scriptapps/fetchruntime.bash new file mode 100644 index 00000000..c687af50 --- /dev/null +++ b/src/scriptapps/fetchruntime.bash @@ -0,0 +1,50 @@ + +runtime_available = 0 +if [[ "$OSTYPE" == "linux"* ]]; then + os="linux" +elif [[ "$OSTYPE" == "darwin"* ]]; then + os="macosx" +elif [[ "$OSTYPE" == "freebsd"* ]]; then + os="freebsd" +elif [[ "$OSTYPE" == "dragonflybsd"* ]]; then + os="dragonflybsd" +elif [[ "$OSTYPE" == "netbsd"* ]]; then + os="netbsd" +elif [[ "$OSTYPE" == "win32" ]]; then + os="win32" + runtime_available = 1 + url="https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclsh901t.exe" + #scriptdir? + output="../src/runtime/tclsh901t.exe" +elif [[ "$OSTYPE" == "msys" ]]; then + echo MSYS + os="win32" + runtime_available = 1 + #use 'command -v' (shell builtin preferred over external which) + interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` + 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}}" + url="https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclsh901t.exe" + #scriptdir? + output="../src/runtime/tclsh901t.exe" +else + #os="$OSTYPE" + os="other" +fi + +if [[ "$runtime_available" -eq 1 ]]; then + #test win32 + echo "Attempting to download $url" + + curl -SL "$output" "$url" + if [[ $? -eq 0 ]]; then + echo "File downloaded to $output" + else + echo "Error: Failed to download to $output" + fi +else + echo "No runtime currently available for $os" +fi diff --git a/src/scriptapps/fetchruntime.ps1 b/src/scriptapps/fetchruntime.ps1 index 67dc5373..5f827e00 100644 --- a/src/scriptapps/fetchruntime.ps1 +++ b/src/scriptapps/fetchruntime.ps1 @@ -1,11 +1,26 @@ -$url = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclkit86bi.exe" -$output = "$(join-path $PSScriptRoot "..\runtime\tclkit86bi.exe")" - -#padding +$url = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclsh901t.exe" +$output = "$(join-path $PSScriptRoot "..\src\runtime\tclsh901t.exe")" +if (-not(Test-Path -Path $output -PathType Leaf)) { + if ((Get-Service -name "BITS").Status -eq "Running") { + Write-Host "OK - BITS services is running" + } else { + #If not running we can get a misleading error such as 'MUI error' + Write-Host "WARNING - BITS service does not seem to be running" + $answer = Read-Host "Do you want to start the Background Intelligent Transfer Service (BITS)? (Type Yes to start service)" + if ($answer -match "yes") { + #restart requires admin - use script marked for admin + #Restart-Service BITS + #Start-Process -FilePath "cmd.exe" -ArgumentList "-NoExit -NoNewWindow -File bits.cmd" + Start-Process -FilePath "bits.cmd" -ArgumentList "-NoExit -NoNewWindow" -Wait + #cmd /c bits.cmd + } else { + write-host "Unable to download. Exiting script.." + exit 1 + } + } -if (-not(Test-Path -Path $output -PathType Leaf)) { try { #Invoke-WebRequest $url -OutFile diff --git a/src/scriptapps/fetchruntime.tcl b/src/scriptapps/fetchruntime.tcl new file mode 100644 index 00000000..2a00bc12 --- /dev/null +++ b/src/scriptapps/fetchruntime.tcl @@ -0,0 +1,42 @@ +package require http +package require tls +http::register https 443 [list ::tls::socket -autoservername true] +package require platform +set plat [platform::identify] +set os [lindex [split $plat -] 0] +set runtime_available 0 +set scriptdir [file dirname [info script]] +switch -- $os { + "win32" { + set url "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclsh901t.exe" + set output [file join $scriptdir "../src/runtime/tclsh901t.exe"] + set runtime_available 1 + } + "linux" { + puts stderr "No runtime currently available for linux" + } + "macosx" { + puts stderr "No runtime currently available for linux" + } + "freebsd" { + puts stderr "No runtime currently available for freebsd" + } + default { + puts stderr "No runtime currently available for $os" + } +} + +if {$runtime_available} { + if {[file exists $output]} { + puts stderr "Runtime already found at $output" + exit 1 + } + puts stdout "Attempting to download $url" + set fd [open $output wb] + set tok [http::geturl $url -channel $fd -binary 1] + close $fd + if {[http::status $tok] eq "ok" && [http::ncode $tok] == 200} { + puts "Download complete." + } + http::cleanup $tok +} diff --git a/src/scriptapps/fetchruntime_old.ps1 b/src/scriptapps/fetchruntime_old.ps1 new file mode 100644 index 00000000..823336c8 --- /dev/null +++ b/src/scriptapps/fetchruntime_old.ps1 @@ -0,0 +1,22 @@ +$url = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master/win64/tclkit901t.exe" +$output = "$(join-path $PSScriptRoot "..\runtime\tclkit901t.exe")" + +#padding + + + +if (-not(Test-Path -Path $output -PathType Leaf)) { + try { + #Invoke-WebRequest $url -OutFile + + Import-Module BitsTransfer + Start-BitsTransfer -Source $url -Destination $output + Write-Host "Runtime saved at $output" + } + catch { + throw $_.Exception.Message + } +} +else { + Write-Host "Runtime already found at $output" +} \ No newline at end of file diff --git a/src/scriptapps/fetchruntime_wrap.toml b/src/scriptapps/fetchruntime_wrap.toml new file mode 100644 index 00000000..0cc604dd --- /dev/null +++ b/src/scriptapps/fetchruntime_wrap.toml @@ -0,0 +1,21 @@ + +[application] + template="punk.multishell.cmd" + as_admin=false + + scripts=[ + "fetchruntime.ps1", + "fetchruntime.tcl", + "fetchruntime.bash" + ] + + default_outputfile="fetchruntime.cmd" + default_nextshellpath="/usr/bin/env bash" + default_nextshelltype="bash" + + #valid nextshelltype entries are: tcl perl powershell bash. + #nextshellpath entries must be 64 characters or less. + + win32.nextshellpath="pwsh" + win32.nextshelltype="powershell" + win32.outputfile="fetchruntime.cmd" diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm index 4f1af2bc..ef3e2a2f 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/commandset/scriptwrap-0.1.0.tm @@ -53,6 +53,7 @@ package require punk::args package require punk::mix package require punk::mix::base package require punk::fileline +package require punk::ansi #*** !doctools @@ -78,6 +79,7 @@ namespace eval punk::mix::commandset::scriptwrap { #[list_begin definitions] namespace export {[a-z]*} + namespace import ::punk::ansi::a ::punk::ansi::a+ namespace eval fileline { namespace import ::punk::fileline::lib::* @@ -807,7 +809,7 @@ namespace eval punk::mix::commandset::scriptwrap { } return $result } - #specific filepath to just wrap one script at the xxx-pre-launch-suprocess site + #specific filepath to just wrap one script at the xxx-payload site #scriptset name to substitute multiple scriptset.xxx files at the default locations - or as specified in scriptset.wrapconf #set usage "" #append usage "Use directly with the script file to wrap, or supply the name of a scriptset" \n @@ -830,6 +832,17 @@ namespace eval punk::mix::commandset::scriptwrap { if {[tomlish::dict::path::exists $tomldict {.application.template}]} { dict set resultd template [tomlish::dict::path::get $tomldict {.application.template.value}] } + + if {[tomlish::dict::path::exists $tomldict {.application.as_admin}]} { + set val [tomlish::dict::path::get $tomldict {.application.as_admin.value}] + if {$val && 1} { + dict set resultd as_admin 1 + } else { + dict set resultd as_admin 0 + } + } else { + dict set resultd as_admin 0 + } set scripts [list] if {[tomlish::dict::path::exists $tomldict {.application.scripts.value}]} { set arrvalues [tomlish::dict::path::get $tomldict {.application.scripts.value}] @@ -872,6 +885,60 @@ namespace eval punk::mix::commandset::scriptwrap { return $resultd } + proc _default_configd {} { + set configd [dict create] + dict set configd template "" + dict set configd as_admin 0 + dict set configd scripts [list] + dict set configd default_outputfile "" + dict set configd default_nextshellpath "" + dict set configd default_nextshelltype "none" + foreach os {win32 dragonflybsd freebsd netbsd linux macosx other} { + dict set configd $os outputfile "" + dict set configd $os nextshellpath "" + dict set configd $os nextshelltype "none" + } + return $configd + } + proc _get_nextshell_script {configd} { + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" + #@SET "nextshelltype[win32___________]=tcl_____________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[dragonflybsd____]=tcl_____________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[freebsd_________]=tcl_____________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[netbsd__________]=tcl_____________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[linux___________]=tcl_____________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[macosx__________]=tcl_____________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" + #@SET "nextshelltype[other___________]=tcl_____________" + + #delimeters + #: <> + #: <> + set script "" + dict for {k v} $configd { + if {[llength $v] % 2 == 0 && [dict exists $v nextshelltype]} { + set os $k + set n [expr {16 - [string length $os]}] + set _os [string repeat _ $n] + set path [dict get $v nextshellpath] + set n [expr {64 - [string length $path]}] + set _path [string repeat _ $n] + set type [dict get $v nextshelltype] + set n [expr {16 - [string length $type]}] + set _type [string repeat _ $n] + append script "@SET \"nextshellpath\[$os$_os\]=$path$_path\"" \n + append script "@SET \"nextshelltype\[$os$_os\]=$type$_type\"" \n + } + } + set script [string trimright $script \n] + return $script + } + punk::args::define { @id -id ::punk::mix::commandset::scriptwrap::multishell @cmd -name punk::mix::commandset::scriptwrap::multishell\ @@ -914,22 +981,22 @@ namespace eval punk::mix::commandset::scriptwrap { -returnextra -type boolean -default 0 @values -minvalues 0 -maxvalues 0 } - #: - #@SET "nextshellpath[win32___________]=tclsh___________________________" + #: <> + #@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" #@SET "nextshelltype[win32___________]=tcl_____________" - #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[dragonflybsd____]=tcl_____________" - #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[freebsd_________]=tcl_____________" - #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[netbsd__________]=tcl_____________" - #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[linux___________]=tcl_____________" - #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[macosx__________]=tcl_____________" - #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" + #@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" #@SET "nextshelltype[other___________]=tcl_____________" - #: + #: <> proc multishell {args} { set argd [punk::args::parse $args withid ::punk::mix::commandset::scriptwrap::multishell] lassign [dict values $argd] leaders opts values received @@ -947,7 +1014,7 @@ namespace eval punk::mix::commandset::scriptwrap { set startdir [pwd] set allowed_extensions [list tcl ps1 sh bash pl] #TODO - distinct sections for sh vs bash? needs experiments.. - #for now we use shell-pre-launch-subprocess etc + #for now we use shell-payload etc #set extension_langs [list tcl tcl ps1 powershell sh sh bash bash pl perl] set extension_langs [list tcl tcl ps1 powershell sh shell bash shell pl perl] @@ -990,13 +1057,15 @@ namespace eval punk::mix::commandset::scriptwrap { } set list_input_files [list] + set has_config 0 set configd [dict create] if {$scriptset ne ""} { puts stdout "Attempting to process all scripts belonging to scriptset '$scriptset'" #.toml file may or may not exist if {[file exists ${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptdir/${scriptset}_wrap.toml" + puts stdout "[a bold green]Loading configuration from $scriptdir/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptdir/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1008,6 +1077,7 @@ namespace eval punk::mix::commandset::scriptwrap { return false } } else { + set configd [_default_configd] puts stdout "No config file for scriptset (must be named ${scriptset}_wrap.toml" puts stdout "Will look for the following scripts in $scriptdir" foreach e $allowed_extensions { @@ -1018,12 +1088,17 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptdir/$scriptset.$e } } + dict set configd scripts $list_input_files } } else { + set configd [_default_configd] #expect a single script if {[file exists $specified_path]} { lappend list_input_files $specified_path } + dict set configd scripts [lmap f $list_input_files {file tail $f}] + set ftail [file tail $specified_path] + dict set configd default_outputfile [file rootname $ftail] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1061,8 +1136,9 @@ namespace eval punk::mix::commandset::scriptwrap { if {$scriptset ne ""} { #.toml file may or may not exist if {[file exists $scriptroot/${scriptset}_wrap.toml]} { - puts stdout "Loading configuration from $scriptroot/${scriptset}_wrap.toml" + puts stdout "[a green]Loading configuration from $scriptroot/${scriptset}_wrap.toml[a]" set configd [_read_scriptset_wrap_tomlfile $scriptroot/${scriptset}_wrap.toml] + set has_config 1 if {[dict exists $configd scripts]} { set configured_scripts [dict get $configd scripts] foreach s $configured_scripts { @@ -1084,6 +1160,8 @@ namespace eval punk::mix::commandset::scriptwrap { lappend list_input_files $scriptroot/$scriptset.$e } } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } } else { #expect a single script @@ -1094,6 +1172,8 @@ namespace eval punk::mix::commandset::scriptwrap { } lappend list_input_files $scriptroot/$filepath_or_scriptset } + set configd [_default_configd] + dict set configd scripts [lmap f $list_input_files {file tail $f}] } set found_script [expr {[llength $list_input_files] > 0}] @@ -1111,7 +1191,7 @@ namespace eval punk::mix::commandset::scriptwrap { } #assertion - customwrapper_folder var exists - but might be empty - if {[dict exists $configd template]} { + if {[dict get $configd template] ne ""} { set templatename [dict get $configd template] } else { if {$opt_template eq "\uFFFF"} { @@ -1119,6 +1199,7 @@ namespace eval punk::mix::commandset::scriptwrap { } else { set templatename $opt_template } + dict set configd template $templatename } set templatename_root [file rootname [file tail $templatename]] @@ -1199,21 +1280,26 @@ namespace eval punk::mix::commandset::scriptwrap { #todo #output_file extension may also depend on the template being used.. and/or the _wrap.toml config - if {[dict size $configd]} { - package require platform - set thisplatform [string tolower [platform::identify]] - set ptype [lindex [split $thisplatform -] 0] - switch -- $ptype { - win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} - default { - set ptype other - } + package require platform + set thisplatform [string tolower [platform::identify]] + set ptype [lindex [split $thisplatform -] 0] + switch -- $ptype { + win32 - dragonflybsd - freebsd - netbsd - linux - macosx {} + default { + set ptype other } + } + if {$has_config} { set out [dict get $configd $ptype outputfile] - set output_file [file join $output_folder $out] + if {[string trim $out] ne ""} { + set output_file [file join $output_folder $out] + } else { + #can be empty for this os if configured that way in xxx_wrap.toml + set output_file "" + } } else { #no _wrap.toml file available - if {$::tcl_platform(platform) eq "windows"} { + if {$ptype eq "win32"} { set output_extension .cmd } else { set output_extension .sh @@ -1224,6 +1310,11 @@ namespace eval punk::mix::commandset::scriptwrap { set infile [lindex $list_input_files 0] set output_file [file join $output_folder [file rootname [file tail $infile]]$output_extension] } + dict set configd $ptype outputfile [file tail $output_file] + } + if {$output_file eq ""} { + puts stderr "No output file configured for platform $ptype" + return } @@ -1236,7 +1327,7 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout "wrap_in_multishell: target file $output_file already exists. File size: [$objFile_existing chunklen] Line count: [$objFile_existing linecount]" if {!$opt_force} { if {$opt_askme} { - set answer [util::askuser "Do you want to overwrite $output_file? Y|N"] + set answer [util::askuser "Do you want to [a bold white Red]overwrite[a] [a bold red]$output_file[a]? Y|N"] if {[string tolower $answer] ne "y"} { puts stderr "aborting due to user response '$answer' (required Y or y to proceed) use -force 1 or -askme 0 to avoid prompts." $objFile_existing destroy @@ -1264,92 +1355,122 @@ namespace eval punk::mix::commandset::scriptwrap { puts stdout $ln } puts stdout "-----------------------------------------------\n" - #foreach ln $template_lines { - #} - - if {[llength $list_input_files] > 1} { - #todo - puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" - return false - } - - #todo - split template at each etc marker and build a dict of parts + #if {[llength $list_input_files] > 1} { + # #todo + # puts stderr "Sorry - only single input file supported. Supply a file extension or use a _wrap.toml config with a single input file for now - implementation incomplete" + # return false + #} - #hack - process one input - set filepath [lindex $list_input_files 0] - set fdscript [open $filepath r] - fconfigure $fdscript -translation binary - set script_data [read $fdscript] - close $fdscript - puts stdout "Read [string length $script_data] bytes of template data.." - set script_lines [split $script_data \n] - puts stdout "Displaying first 3 lines of your script between dashed lines..." - puts stdout "-----------------------------------------------" - foreach ln [lrange $script_lines 0 3] { - puts stdout $ln - } - puts stdout "-----------------------------------------------\n" - puts stdout "Target for above script data is '$output_file'" - set script_ext [string trim [file extension $filepath] .] - set lang [dict get $extension_langs [string tolower $script_ext]] - puts stdout "Language of script being wrapped is $lang" - if {$opt_askme} { - set answer [util::askuser "Does this look correct? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." - return + set lang_data [dict create] + foreach filepath $list_input_files { + set script_ext [string trim [file extension $filepath] .] + set lang [dict get $extension_langs [string tolower $script_ext]] + set fdscript [open $filepath r] + fconfigure $fdscript -translation binary + set script_data [read $fdscript] + close $fdscript + puts stdout "Read [string length $script_data] bytes of template data for lang: $lang" + set script_lines [split $script_data \n] + dict set lang_data $lang $script_lines + puts stdout "Displaying first 3 lines of your script [file tail $filepath] between dashed lines..." + puts stdout "[a green]$filepath[a]" + puts stdout "-----------------------------------------------" + foreach ln [lrange $script_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + puts stdout "Target for script data is '$output_file'" + puts stdout "Language of script being wrapped is [a bold yellow]$lang[a]" + if {$opt_askme} { + set answer [util::askuser "Does this look correct? Y|N"] + if {[string tolower $answer] ne "y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } } } - set start_idx 0 - set end_idx 0 - set line_idx 0 - set existing_payload [list] + set template_ranges [list] + set data_items [list] + set trange [list 0] + set line_idx -1 + set opentag "" foreach ln $template_lines { - - if {[string match "#<$lang-pre-launch-subprocess>*" $ln]} { - set start_idx $line_idx - } elseif {[string match "#*" $ln]} { - set end_idx $line_idx - break - } elseif {$start_idx > 0} { - if {$end_idx > 0} { - lappend existing_payload [string trim $ln] - } + incr line_idx + if {$opentag eq ""} { + if {[string match ": <>*" $ln]} { + set opentag asadmin + lset trange 1 $line_idx ;#include tag in template + lappend template_ranges $trange + set trange [list $line_idx] + set asadmin [dict get $configd as_admin] + set scr "@SET \"asadmin=$asadmin\"" + lappend data_items $scr + } elseif {[string match ": <>*" $ln]} { + set opentag nextshell + lset trange 1 $line_idx + lappend template_ranges $trange + set trange [list $line_idx] + set nextshell_script [_get_nextshell_script $configd] + #puts stderr "-------------------" + #puts stderr "$nextshell_script" + lappend data_items $nextshell_script + } elseif {[string match "#<*-payload>*" $ln]} { + regexp {#<(.*)-payload>.*} $ln _ lang + if {[dict exists $lang_data $lang]} { + set script_lines [dict get $lang_data $lang] + set opentag payload-$lang + lset trange 1 $line_idx + lappend template_ranges $trange + lappend data_items [join $script_lines \n] + } + } } else { - + switch -- [string range $opentag 0 6] { + asadmin { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + nextshe { + if {[string match ": <>*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + payload { + set lang [string range $opentag 8 end] ;#payload-xxx + if {[string match "#*" $ln]} { + set trange [list $line_idx] + set opentag "" + } + } + } } - incr line_idx } - if {($start_idx == 0) || ($end_idx == 0)} { - error "wrap_in_multishell was unable to find payload area in template marked with #<$lang-pre-launch-subprocess> and # on separate lines" + if {$opentag eq ""} { + lset trange 1 end + lappend template_ranges $trange + } else { + error "multishell - unable to find closing tag for '$opentag'" } - set existing_string [join $existing_payload \n] - if {[string length [string trim $existing_string]]} { - puts stdout "EXISTING <$lang-pre-launch-subprocess> PAYLOAD!!" - puts stdout "-----------------------------------------------\n" - puts stdout $existing_string - puts stdout "-----------------------------------------------\n" - error "wrap_in_multishell found existing payload for language $lang ... aborting." - #todo - allow overwrite only in files outside of punkshell distribution? - if 0 { - puts stderr "Found existing $lang payload.. overwrite?" - if {$opt_askme} { - set answer [util::askuser "Are you sure you want to replace the $lang payload shown above? Y|N"] - if {[string tolower $answer] ne "y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts." - return - } - } + set newscript "" + foreach trange $template_ranges item $data_items { + append newscript [join [lrange $template_lines {*}$trange] \n] + if {$item ne ""} { + append newscript \n $item \n + } else { + append newscript \n } } - set tpl_head_lines [lrange $template_lines 0 $start_idx] ;#include tag line - set tpl_tail_lines [lrange $template_lines $end_idx end] - set newscript [join $tpl_head_lines \n]\n[join $script_lines \n]\n[join $tpl_tail_lines \n] + + puts stdout "New script is [string length $newscript] bytes" puts stdout $newscript + set fdtarget [open $output_file w] fconfigure $fdtarget -translation binary puts -nonewline $fdtarget $newscript 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 9daf7ebf..8fb75ca3 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,5 +1,5 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ -set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set S;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;S 1 list]"; set -- : "$@";$1 = @' : heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ : .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ : "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + @@ -16,41 +16,41 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. -@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder -@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) -@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called. +@REM Change the value of nextshell to one of the supported types, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or bash,tcl,perl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: dev scriptwrap.multishell -outputfolder +@REM Call with sh, bash, perl, or tclsh. (powershell untested on unix) +@REM Due to lack of shebang (#! line) Unix-like systems will hopefully default to a flavour of sh that can divert to bash if the script is called without an interpreter - but it may depend on the shell in use when called. @REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. @REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. @REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________" +@SET "validshelltypes= 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 32 chars needed for a target, it can still be done but overall script padding may need checking/adjusting +@REM If more than 64 chars needed for a target, it can still be done but overall script padding may need checking/adjusting @REM Supporting more explicit oses than those listed may also require script padding adjustment -: -@SET "nextshellpath[win32___________]=tclsh___________________________" +: <> +@SET "nextshellpath[win32___________]=tclsh___________________________________________________________" @SET "nextshelltype[win32___________]=tcl_____________" -@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" +@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[dragonflybsd____]=tcl_____________" -@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[freebsd_________]=tcl_____________" -@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[netbsd__________]=tcl_____________" -@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[linux___________]=tcl_____________" -@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[macosx__________]=tcl_____________" -@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" +@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________________________________________" @SET "nextshelltype[other___________]=tcl_____________" -: +: <> @rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). -: +: <> @SET "asadmin=0" -: +: <> @REM @ECHO nextshelltype is %nextshelltype[win32___________]% @REM @SET "selected_shelltype=%nextshelltype[win32___________]%" @SET "selected_shelltype=%nextshelltype[win32___________]%" @@ -143,7 +143,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @ECHO args = args ^& strArg ^& " " >> "%vbsGetPrivileges%" @ECHO Next >> "%vbsGetPrivileges%" @ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" -@ECHO Launching script in new windows due to administrator elevation +@ECHO Launching script in new window due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -175,8 +175,11 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$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%"=="powershell" ( - REM pws vs powershell hasn't been tested because we didn't need to copy cmd to ps1 this time +@IF "!selected_shelltype_trimmed!"=="none" ( + SET selected_shelltype_trimmed=powershell +) +@IF "!selected_shelltype_trimmed!"=="powershell" ( + 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; write-host "statusmessage: pwsh-found" >NUL SET pwshtest_exitcode=!errorlevel! @@ -192,7 +195,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' SET task_exitcode=!errorlevel! ) ) ELSE ( - IF "%selected_shelltype_trimmed%"=="wslbash" ( + IF "!selected_shelltype_trimmed!"=="wslbash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% @@ -203,6 +206,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' REM sh on windows uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode + @ECHO HERE "!selected_shelltype_trimmed!" "!selected_shellpath_trimmed!" %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call; ) ELSE ( ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes% @@ -383,14 +387,15 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET "rtrn=%~2" @SET "string=%~1" @SET "trimstring=%~1" - @REM trim up to 31 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:###=% + @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 "" ( @@ -439,7 +444,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' # -- e.g tclsh filename.cmd # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### -rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore +rename set ""; rename S set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore Hide :exit_multishell;Hide {<#};Hide '@ namespace eval ::punk::multishell { set last_script_root [file dirname [file normalize ${::argv0}/__]] @@ -473,6 +478,9 @@ namespace eval ::punk::multishell { #puts "argv0 : $::argv0" # -- --- --- --- --- --- --- --- --- --- --- --- +# +puts stderr "No tcl code for this script. Try another program such as perl or bash" +# # # @@ -502,8 +510,20 @@ if {[::punk::multishell::is_main]} { # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload # end hide from unix shells \ HEREDOC1B_HIDE_FROM_BASH_AND_SH +# csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ +echo "script: `echo $0 | sed 's/^-//'`" +# csh/tcsh/sh/bash use oldschool backticks and sed lowest common denominator \ +echo "shell: " `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` +#csh/tcsh diversion \ +test "$argv[*]" != "[*]" && ( /usr/bin/env bash $argv[*]; exit ) +#other non-bash diversion \ +test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && /usr/bin/env bash $0 +#review \ +test `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` != "bash" && exit # sh/bash \ shift && set -- "${@:1:$#-1}" + +#echo "shell:" `ps -o args= $$ | sed -E 's/^.*\/|^-//' | awk '{print $1}'` #------------------------------------------------------ # -- This if block only needed if Tcl didn't exit or return above. if false==false # else { @@ -518,10 +538,80 @@ if false==false # else { # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload + +if [[ "$OSTYPE" == "linux"* ]]; then + os="linux" +elif [[ "$OSTYPE" == "darwin"* ]]; then + os="macosx" +elif [[ "$OSTYPE" == "freebsd"* ]]; then + os="freebsd" +elif [[ "$OSTYPE" == "dragonflybsd"* ]]; then + os="dragonflybsd" +elif [[ "$OSTYPE" == "netbsd"* ]]; then + os="netbsd" +elif [[ "$OSTYPE" == "win32" ]]; then + os="win32" +elif [[ "$OSTYPE" == "msys" ]]; then + echo MSYS + os="win32" + #review - need ps/sed/awk to determine shell? + interp = `ps -p $$ | awk '$1 != "PID" {print $(NF)}' | tr -d '()' | sed -E 's/^.*\/|^-//'` + #use 'command -v' (shell builtin preferred over external which) + shellpath=`command -v $interp` + shellfolder="${shellpath%/*}" #avoid dependency on basename or dirname + #"c:/windows/system32/" is quite likely in the path ahead of msys,git etc. + #This breaks calls to various unix utils such as sed etc (wsl related?) + export PATH="$shellfolder${PATH:+:${PATH}}" +else + #os="$OSTYPE" + os="other" +fi +echo ostype: $OSTYPE +shellconfigline=$( sed -n "/: <>/{:a;n;/: <>/q;p;ba}" "$0" | grep $os) +#echo $shellconfigline; +if [[ $shellconfigline == *"nextshelltype"* ]]; then + echo "found config for os $os" + split1="${shellconfigline#*=}" #remove everything through the first '=' + #echo "split1: $split1" + pathraw="${split1%%\"*}" #take everything before the quote - use %% to get longest match + pathraw="${pathraw//\"/}" #remove quote + nextshellpath="${pathraw/%_*/}" #remove trailing underscores (% = must match at end) + #echo "nextshellpath: $nextshellpath" + split2="${split1#*=}" + #echo "split2: $split2" + split2="${split2//\"/}" + nextshelltype="${split2/%_*/}" + echo "nextshelltype: $nextshelltype" +else + echo "unable to find config for os $os" + echo "shellconfigline: $shellconfigline" + nextshellpath="" + nextshelltype="" +fi exitcode=0 +#-- sh/bash launches nextscript here instead of shebang line at top +if [[ "$nextshelltype" != "bash" && "$nextshelltype" != "none" ]]; then + #echo bash launching subshell of type $nextshelltype $nextshellpath on "$0" + #/usr/bin/env tclsh "$0" "$@" + ${nextshellpath} "$0" "$@" + + exitcode=$? + #echo "sh/bash reporting exitcode: ${exitcode}" + exit $exitcode + #-- override exitcode example + #exit 66 +else + #already in bash - don't launch another process or we would loop + #echo "bash payload" + : +fi +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload #printf "start of bash or sh code" +# +echo "No bash code for this script. Try another program such as perl or tcl" >&2 +# + # # @@ -531,8 +621,8 @@ exitcode=0 #-- 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=$? +#/usr/bin/env tclsh "$0" "$@" +#exitcode=$? #echo "sh/bash reporting tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 @@ -558,8 +648,18 @@ exit ${exitcode} # ## ### ### ### ### ### ### ### ### ### ### ### ### ### =cut #!/user/bin/perl -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload 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(); @@ -571,13 +671,15 @@ my $exit_code = 0; -my $scriptname = $0; -print "perl $scriptname\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"; +# + # # @@ -585,7 +687,7 @@ foreach my $a(@ARGV) { # -- --- --- --- --- --- --- --- # -$exit_code=system("tclsh", $scriptname, @ARGV); +#$exit_code=system("tclsh", $scriptname, @ARGV); #print "perl reporting tcl exitcode: $exit_code"; # # -- --- --- --- --- --- --- --- @@ -680,12 +782,34 @@ function GetDynamicParamDictionary { # } #} #psmain @args -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host #"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- +$startTag = ": <>" +$endTag = ": <>" +$fileContent = Get-Content $scriptname -Raw +$pattern = "(?s)$startTag(.*?)$endTag" +$matches = [regex]::Matches($fileContent,$pattern) +$admininfo = $matches[0].Groups[1].Value +$asadmin = 0 +if ($matches.count) { + $asadmin = $admininfo.Contains("asadmin=1") + if ($asadmin) { + if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + # If not elevated, relaunch with elevated privileges + # -Wait e.g for starting a service or other operations which remainder of script may depend on + Start-Process -FilePath "pwsh.exe" -ArgumentList "-NoProfile -NoExit -ExecutionPolicy Bypass -File $($MyInvocation.MyCommand.Path)" -Wait -Verb RunAs + Exit # Exit the current non-elevated process + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload + +# +Write-Error "No powershell code for this script. Try another program such as perl, tcl or bash" +# # # @@ -693,7 +817,7 @@ function GetDynamicParamDictionary { # -- --- --- --- --- --- --- --- # -tclsh $scriptname $args +#tclsh $scriptname $args #"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host # # -- --- --- --- --- --- --- --- diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd index 17fe4c15..9daf7ebf 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell1.cmd @@ -1,41 +1,65 @@ -: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^ -set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; 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 @ECHO; Hide ); Hide (;Hide echo; Hide @REM]#not necessary but can help avoid errs in testing" +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ +: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + : << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. +: shebang line is not required on unix or windows and will reduce functionality and/or portability. +: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. +@GOTO :skip_perl_pod_start ^; +=begin excludeperl +: skip_perl_pod_start : Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ : { -: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. 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. @REM ############################################################################################################################ -@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, sh and/or powershelll (powershell.exe or pwsh.exe) +@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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: pmix scriptwrap.multishell -outputfolder +@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder @REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) @REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh if the script is called without an interpreter - but it may depend on the shell in use when called. @REM If you find yourself really wanting/needing to add a shebang line - do so on the basis that the script will exist on unix-like systems only. +@REM in batch scripts - array syntax with square brackets is a simulation of arrays or associative arrays. +@REM note that many shells linked as sh do not support substition syntax and may fail - e.g dash etc - generally bash should be used in this context @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh'" -@SET "shells[10]=pwsh" -@SET "shells[11]=sh" -@set "shells[12]=bash" -@SET "shells[13]=tclsh" +@SET "validshelltypes= powershell______ sh______________ wslbash_________ bash____________ tcl_____________ perl____________" +@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 32 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 "nextshell=13" +@SET "nextshellpath[win32___________]=tclsh___________________________" +@SET "nextshelltype[win32___________]=tcl_____________" +@SET "nextshellpath[dragonflybsd____]=/usr/bin/env tclsh______________" +@SET "nextshelltype[dragonflybsd____]=tcl_____________" +@SET "nextshellpath[freebsd_________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[freebsd_________]=tcl_____________" +@SET "nextshellpath[netbsd__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[netbsd__________]=tcl_____________" +@SET "nextshellpath[linux___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[linux___________]=tcl_____________" +@SET "nextshellpath[macosx__________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[macosx__________]=tcl_____________" +@SET "nextshellpath[other___________]=/usr/bin/env tclsh______________" +@SET "nextshelltype[other___________]=tcl_____________" : @rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). : @SET "asadmin=0" : -@REM nextshell set to index for validshells .eg 10 for pwsh -@REM @ECHO nextshell is %nextshell% -@SET "selected=!shells[%nextshell%]!" -@REM @ECHO selected %selected% -@CALL SET "keyRemoved=%%validshells:'!selected!'=%%" +@REM @ECHO nextshelltype is %nextshelltype[win32___________]% +@REM @SET "selected_shelltype=%nextshelltype[win32___________]%" +@SET "selected_shelltype=%nextshelltype[win32___________]%" +@REM @ECHO selected_shelltype %selected_shelltype% +@CALL :stringTrimTrailingUnderscores %selected_shelltype% selected_shelltype_trimmed +@REM @ECHO selected_shelltype_trimmed %selected_shelltype_trimmed% +@SET "selected_shellpath=%nextshellpath[win32___________]%" +@CALL :stringTrimTrailingUnderscores %selected_shellpath% selected_shellpath_trimmed +@CALL SET "keyRemoved=%%validshelltypes:!selected_shelltype!=%%" @REM @ECHO keyremoved %keyRemoved% @REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available @REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### @@ -49,16 +73,16 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @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 -- pmix scriptwrap.checkfile +@REM -- deck scriptwrap.checkfile @REM -- to check your templates or final wrapped scripts for byte boundary issues @REM -- It will report any labels that are on boundaries @REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. -@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using pmix scriptwrap.checkfile is still recommended. +@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 pmix scriptwrap.checkfile doesn't check all such boundaries. +@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 @@ -89,22 +113,36 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) @SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" @SET arglist=%* -@IF "%1"=="PUNK-ELEVATED" ( +@SET "qstrippedargs=args%arglist%" +@SET "qstrippedargs=%qstrippedargs:"=%" +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( GOTO :gotPrivileges ) @IF !asadmin!==1 ( net file 1>NUL 2>NUL @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) ) +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding +@REM padding @GOTO skip_privileges :getPrivileges -@IF '%1'=='PUNK-ELEVATED' (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) +@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.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%" +@ECHO UAC.ShellExecute "%~dp0%~n0%~x0", args, "", "runas", 1 >> "%vbsGetPrivileges%" @ECHO Launching script in new windows due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -113,7 +151,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @REM setlocal & pushd . @PUSHD . @cd /d %~dp0 -@IF "%1"=="PUNK-ELEVATED" ( +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( @DEL "%vbsGetPrivileges%" 1>nul 2>nul @SET arglist=%arglist:~14% ) @@ -124,7 +162,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @if not exist "%~dp0%~n0.ps1" ( @SET need_ps1=1 ) ELSE ( - fc "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >nul || goto different + fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different @REM @ECHO "files same" @SET need_ps1=0 ) @@ -134,10 +172,10 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @SET need_ps1=1 :pscontinue @IF !need_ps1!==1 ( - COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL + 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 "!shells[%nextshell%]!"=="pwsh" ( +@IF "%selected_shelltype_trimmed%"=="powershell" ( REM pws 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; write-host "statusmessage: pwsh-found" >NUL @@ -145,7 +183,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe REM ECHO pwshtest_exitcode !pwshtest_exitcode! REM fallback to powershell if pwsh failed IF !pwshtest_exitcode!==0 ( - pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel! + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! ) ELSE ( REM CALL powershell -nop -nol -c write-host powershell-found REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* @@ -153,24 +192,31 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe SET task_exitcode=!errorlevel! ) ) ELSE ( - IF "!shells[%nextshell%]!"=="bash" ( + IF "%selected_shelltype_trimmed%"=="wslbash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" - !shells[%nextshell%]! "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel! + %selected_shellpath_trimmed% "!wslpath!%fname%" %arglist% + SET task_exitcode=!errorlevel! ) ELSE ( - REM probably tclsh or sh - IF NOT "x%keyRemoved%"=="x%validshells%" ( + 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 - !shells[%nextshell%]! "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! + REM The compound statement with trailing call is required to stop batch termination confirmation, whilst still capturing exitcode + %selected_shellpath_trimmed% "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! & Call; ) ELSE ( - ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% + ECHO %fname% has invalid nextshelltype value %selected_shelltype% valid options are %validshelltypes% SET task_exitcode=66 + @REM boundary padding + @REM boundary padding + @REM boundary padding + @REM boundary padding GOTO :exit_multishell ) ) ) @REM batch file library functions +@REM boundary padding @GOTO :endlib :getWslPath @@ -179,7 +225,9 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @SET "name=%~nx1" @SET "drive=%~d1" @SET "rtrn=%~2" - @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" + @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%" @@ -227,6 +275,7 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) @EXIT /B @REM boundary padding +@REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @@ -245,6 +294,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe @REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' @REM boundary padding @REM boundary padding +@REM boundary padding +@REM boundary padding @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @@ -289,7 +340,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) ) @EXIT /B - +@REM boundary padding +@REM boundary padding :stringToUpper @SETLOCAL @SET "rtrn=%~2" @@ -307,7 +359,47 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe ) ) @EXIT /B - +:stringToLower +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "retstring=%~1" + @FOR %%A in (a b c d e f g h i j k l m n o p q r s t u v w x y z) DO @( + @SET "retstring=!retstring:%%A=%%A!" + ) + @SET "result=!retstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToLower %string% result: %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:stringTrimTrailingUnderscores +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "trimstring=%~1" + @REM trim up to 31 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 "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" @@ -328,6 +420,8 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe :endlib : \ +@REM padding +@REM padding @REM @SET taskexit_code=!errorlevel! & goto :exit_multishell @GOTO :exit_multishell # } @@ -348,9 +442,9 @@ set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershe rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore Hide :exit_multishell;Hide {<#};Hide '@ namespace eval ::punk::multishell { - set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script_root [file dirname [file normalize ${::argv0}/__]] set last_script [file dirname [file normalize [info script]/__]] - if {[info exists argv0] && + 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 @@ -365,7 +459,7 @@ namespace eval ::punk::multishell { 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}/__]]" + puts stderr "Info: script_root: [file dirname [file normalize ${::argv0}/__]]" return 0 } return [set ::punk::multishell::is_main($script_name)] @@ -380,10 +474,16 @@ namespace eval ::punk::multishell { # -- --- --- --- --- --- --- --- --- --- --- --- -# -# +# +# + +# +# +# +# + # -- --- --- --- --- --- --- --- --- --- --- --- # -- Best practice is to always return or exit above, or just by leaving the below defaults in place. @@ -414,33 +514,33 @@ if false==false # else { # -- leave as is if all that is required is launching the Tcl payload" # -- # -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default -# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate +# -- adjust the %nextshell% value above # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +exitcode=0 #printf "start of bash or sh code" -# -# +# +# # -- --- --- --- --- --- --- --- -# -exitcode=0 ;#default assumption +# #-- sh/bash launches Tcl here instead of shebang line at top #-- use exec to use exitcode (if any) directly from the tcl script #exec /usr/bin/env tclsh "$0" "$@" #-- alternative - can run sh/bash script after the tcl call. /usr/bin/env tclsh "$0" "$@" exitcode=$? -#echo "tcl exitcode: ${exitcode}" +#echo "sh/bash reporting tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 -# +# # -- --- --- --- --- --- --- --- -# -# +# +# #printf "sh/bash done \n" @@ -448,7 +548,57 @@ exitcode=$? #------------------------------------------------------ fi exit ${exitcode} -# end hide sh/bash block from Tcl +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- 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 +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload +my $exit_code = 0; +#use ExtUtils::Installed; +#my $installed = ExtUtils::Installed->new(); +#my @modules = $installed->modules(); +#print "Modules:\n"; +#foreach my $m (@modules) { +# print "$m\n"; +#} +# -- --- --- + + + +my $scriptname = $0; +print "perl $scriptname\n"; +my $i =1; +foreach my $a(@ARGV) { + print "Arg # $i: $a\n"; +} + +# +# + + + +# -- --- --- --- --- --- --- --- +# +$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 @@ -460,9 +610,76 @@ if 0 { # -- 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 +$scriptname = GetScriptName +function GetDynamicParamDictionary { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + [string] $CommandName + ) + + begin { + # Get a list of params that should be ignored (they're common to all advanced functions) + $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + } + + process { + # Create the dictionary that this scriptblock will return: + $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Convert to object array and get rid of Common params: + (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | + Where-Object { $CommonParameterNames -notcontains $_.Key } | + ForEach-Object { + $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( + $_.Key, + $_.Value.ParameterType, + $_.Value.Attributes + ) + $DynParamDictionary.Add($_.Key, $DynamicParameter) + } + + # Return the dynamic parameters + return $DynParamDictionary + } +} +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargumentname' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# #... +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargumentname value is: $myargumentname" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host @@ -470,22 +687,22 @@ $scriptname = getScriptName #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- -# +# tclsh $scriptname $args -# +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host +# # -- --- --- --- --- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' @@ -498,7 +715,7 @@ $1 = @' : \ @REM @ECHO exitcode: !task_exitcode! : \ -@IF "%1"=="PUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) : \ @EXIT /B !task_exitcode! # cmd has exited @@ -509,6 +726,7 @@ $1 = @' # -- powershell multiline comment #> <# +no script engine should try to run me # id:tailblock1 #  diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd index a9688b6a..17fe4c15 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell2.cmd @@ -1,34 +1,29 @@ -: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ -set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' -: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ -: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ -: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + +: "[rename set s;proc Hide x {proc $x args {}};Hide :]" "\$(function : {<#pwsh#>})" ^ +set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide x {proc $x args {}}; 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 @ECHO; Hide ); Hide (;Hide echo; 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 \ : { +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT. 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. @REM ############################################################################################################################ @REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, 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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. @REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system -@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder +@REM e.g from within a running punkshell: pmix scriptwrap.multishell -outputfolder @REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) @REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh 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. @SETLOCAL EnableExtensions EnableDelayedExpansion -@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh' (^14^) 'perl'" +@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh'" @SET "shells[10]=pwsh" @SET "shells[11]=sh" @set "shells[12]=bash" @SET "shells[13]=tclsh" -@SET "shells[14]=perl" : @SET "nextshell=13" : @@ -54,16 +49,16 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) @REM -- Even something as simple as adding or removing an @REM @REM -- From within punkshell - use: -@REM -- deck scriptwrap.checkfile +@REM -- pmix scriptwrap.checkfile @REM -- to check your templates or final wrapped scripts for byte boundary issues @REM -- It will report any labels that are on boundaries @REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. -@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- 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 pmix 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 -- It is unknown what versions of cmd interpreters behave this way - and pmix 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 @@ -94,40 +89,22 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$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" ( +@IF "%1"=="PUNK-ELEVATED" ( GOTO :gotPrivileges ) @IF !asadmin!==1 ( net file 1>NUL 2>NUL @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) ) -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM -@REM @GOTO skip_privileges :getPrivileges -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo PUNK-ELEVATED & shift /1 & goto :gotPrivileges ) +@IF '%1'=='PUNK-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 UAC.ShellExecute "%~dp0%~n0.cmd", args, "", "runas", 1 >> "%vbsGetPrivileges%" @ECHO Launching script in new windows due to administrator elevation @"%SystemRoot%\System32\WScript.exe" "%vbsGetPrivileges%" %* @EXIT /B @@ -136,7 +113,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM setlocal & pushd . @PUSHD . @cd /d %~dp0 -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( +@IF "%1"=="PUNK-ELEVATED" ( @DEL "%vbsGetPrivileges%" 1>nul 2>nul @SET arglist=%arglist:~14% ) @@ -147,7 +124,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @if not exist "%~dp0%~n0.ps1" ( @SET need_ps1=1 ) ELSE ( - fc "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >nul || goto different + fc "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >nul || goto different @REM @ECHO "files same" @SET need_ps1=0 ) @@ -157,7 +134,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @SET need_ps1=1 :pscontinue @IF !need_ps1!==1 ( - COPY "%~dp0%~n0%~x0" "%~dp0%~n0.ps1" >NUL + COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL ) @REM avoid using CALL to launch pwsh,tclsh etc - it will intercept some args such as /? @IF "!shells[%nextshell%]!"=="pwsh" ( @@ -168,8 +145,7 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' REM ECHO pwshtest_exitcode !pwshtest_exitcode! REM fallback to powershell if pwsh failed IF !pwshtest_exitcode!==0 ( - pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% - SET task_exitcode=!errorlevel! + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( REM CALL powershell -nop -nol -c write-host powershell-found REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* @@ -180,26 +156,21 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' IF "!shells[%nextshell%]!"=="bash" ( CALL :getWslPath %winpath% wslpath REM ECHO wslfullpath "!wslpath!%fname%" - !shells[%nextshell%]! "!wslpath!%fname%" %arglist% - SET task_exitcode=!errorlevel! + !shells[%nextshell%]! "!wslpath!%fname%" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( REM probably tclsh or sh IF NOT "x%keyRemoved%"=="x%validshells%" ( 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 - !shells[%nextshell%]! "%~dp0%fname%" %arglist% - SET task_exitcode=!errorlevel! + !shells[%nextshell%]! "%~dp0%fname%" %arglist% & SET task_exitcode=!errorlevel! ) ELSE ( ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% SET task_exitcode=66 - @REM boundary padding - @REM boundary padding GOTO :exit_multishell ) ) ) @REM batch file library functions -@REM boundary padding @GOTO :endlib :getWslPath @@ -256,7 +227,6 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' ) @EXIT /B @REM boundary padding -@REM boundary padding :getNormalizedScriptTail @SETLOCAL @SET "result=%~nx0" @@ -275,8 +245,6 @@ set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' @REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' @REM boundary padding @REM boundary padding -@REM boundary padding -@REM boundary padding @SETLOCAL @CALL :stringContains %~1 "\" hasBackSlash @CALL :stringContains %~1 "/" hasForwardSlash @@ -412,15 +380,9 @@ namespace eval ::punk::multishell { # -- --- --- --- --- --- --- --- --- --- --- --- -# -# - -# -# - +# +# -# -# # -- --- --- --- --- --- --- --- --- --- --- --- @@ -452,33 +414,33 @@ if false==false # else { # -- 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 +# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate # -- if sh/bash scripting needs to run on windows too. # -- # ## ### ### ### ### ### ### ### ### ### ### ### ### ### # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload -exitcode=0 #printf "start of bash or sh code" -# -# +# +# # -- --- --- --- --- --- --- --- -# +# +exitcode=0 ;#default assumption #-- sh/bash launches Tcl here instead of shebang line at top #-- use exec to use exitcode (if any) directly from the tcl script #exec /usr/bin/env tclsh "$0" "$@" #-- alternative - can run sh/bash script after the tcl call. /usr/bin/env tclsh "$0" "$@" exitcode=$? -#echo "sh/bash reporting tcl exitcode: ${exitcode}" +#echo "tcl exitcode: ${exitcode}" #-- override exitcode example #exit 66 -# +# # -- --- --- --- --- --- --- --- -# -# +# +# #printf "sh/bash done \n" @@ -486,57 +448,7 @@ exitcode=$? #------------------------------------------------------ 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 -# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload -my $exit_code = 0; -#use ExtUtils::Installed; -#my $installed = ExtUtils::Installed->new(); -#my @modules = $installed->modules(); -#print "Modules:\n"; -#foreach my $m (@modules) { -# print "$m\n"; -#} -# -- --- --- - - - -my $scriptname = $0; -print "perl $scriptname\n"; -my $i =1; -foreach my $a(@ARGV) { - print "Arg # $i: $a\n"; -} - -# -# - - - -# -- --- --- --- --- --- --- --- -# -$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 +# end hide sh/bash block from Tcl # This comment with closing brace should stay in place whether if commented or not } #------------------------------------------------------ # begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above @@ -548,76 +460,9 @@ if 0 { # -- Do not edit if current file is the .ps1 # -- Edit the corresponding .cmd and it will autocopy # -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above -# -- custom script should generally go below the begin_powershell_payload line # ## ### ### ### ### ### ### ### ### ### ### ### ### ### function GetScriptName { $myInvocation.ScriptName } -$scriptname = GetScriptName -function GetDynamicParamDictionary { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline=$true, Mandatory=$true)] - [string] $CommandName - ) - - begin { - # Get a list of params that should be ignored (they're common to all advanced functions) - $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | - Get-Member -MemberType Properties | - Select-Object -ExpandProperty Name - } - - process { - # Create the dictionary that this scriptblock will return: - $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary - - # Convert to object array and get rid of Common params: - (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | - Where-Object { $CommonParameterNames -notcontains $_.Key } | - ForEach-Object { - $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( - $_.Key, - $_.Value.ParameterType, - $_.Value.Attributes - ) - $DynParamDictionary.Add($_.Key, $DynamicParameter) - } - - # Return the dynamic parameters - return $DynParamDictionary - } -} -# GetDynamicParamDictionary -# - This can make it easier to share a single set of param definitions between functions -# - sample usage -#function ParameterDefinitions { -# param( -# [Parameter(Mandatory)][string] $myargument -# ) -#} -#function psmain { -# [CmdletBinding()] -# param() -# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } -# process { -# #called once with $PSBoundParameters dictionary -# #can be used to validate arguments, or set a simpler variable name for access -# switch ($PSBoundParameters.keys) { -# 'myargumentname' { -# Set-Variable -Name $_ -Value $PSBoundParameters."$_" -# } -# #... -# } -# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { -# #... -# } -# } -# end { -# #Main function logic -# Write-Host "myargumentname value is: $myargumentname" -# #myotherfunction @PSBoundParameters -# } -#} -#psmain @args +$scriptname = getScriptName # -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload #"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host #"Script Name : {0}" -f $scriptname | write-host @@ -625,22 +470,22 @@ function GetDynamicParamDictionary { #"powershell args : {0}" -f ($args -join ", ") | write-host # -- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- -# +# tclsh $scriptname $args -#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host -# +# # -- --- --- --- --- --- --- --- -# -# +# +# # -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host Exit $LASTEXITCODE # heredoc2 for powershell to ignore block below $1 = @' @@ -653,7 +498,7 @@ $1 = @' : \ @REM @ECHO exitcode: !task_exitcode! : \ -@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) +@IF "%1"=="PUNK-ELEVATED" (echo. & @cmd /k echo elevated prompt: type exit to quit) : \ @EXIT /B !task_exitcode! # cmd has exited @@ -664,7 +509,6 @@ $1 = @' # -- powershell multiline comment #> <# -no script engine should try to run me # id:tailblock1 #  diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd new file mode 100644 index 00000000..a9688b6a --- /dev/null +++ b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates/utility/scriptappwrappers/multishell3.cmd @@ -0,0 +1,680 @@ +: "punk MULTISHELL - shebangless polyglot for Tcl Perl sh bash cmd pwsh powershell" + "[rename set s;proc Hide x {proc $x args {}};Hide :]" + "\$(function : {<#pwsh#>})" + "perlhide" + qw^ +set -- "$@" "a=[Hide <#;Hide set;s 1 list]"; set -- : "$@";$1 = @' +: heredoc1 - hide from powershell using @ and squote above. close sqote for unix shells + ' \ +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl + \ +: "[Hide @GOTO; Hide =begin; Hide @REM] #not necessary but can help avoid errs in testing" + +: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: STRONG SUGGESTION: DO NOT MODIFY FIRST LINE OF THIS SCRIPT - except for first double quoted section. +: shebang line is not required on unix or windows and will reduce functionality and/or portability. +: Even comment lines can be part of the functionality of this script (both on unix and windows) - modify with care. +@GOTO :skip_perl_pod_start ^; +=begin excludeperl +: skip_perl_pod_start +: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ +: { +@REM ############################################################################################################################ +@REM THIS IS A POLYGLOT SCRIPT - supporting payloads in Tcl, bash, 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 On windows, change the value of nextshell to one of the listed 2 digit values if desired, and add code within payload sections for tcl,sh,bash,powershell as appropriate. +@REM This wrapper can be edited manually (carefully!) - or sh,bash,tcl,powershell scripts can be wrapped using the Tcl-based punkshell system +@REM e.g from within a running punkshell: deck scriptwrap.multishell -outputfolder +@REM On unix-like systems, call with sh, bash or tclsh. (powershell untested on unix - and requires wscript if security elevation is used) +@REM Due to lack of shebang (#! line) Unix-like systems will probably (hopefully) default to sh 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. +@SETLOCAL EnableExtensions EnableDelayedExpansion +@SET "validshells= ^(10^) 'pwsh' ^(11^) 'sh' (^12^) 'bash' (^13^) 'tclsh' (^14^) 'perl'" +@SET "shells[10]=pwsh" +@SET "shells[11]=sh" +@set "shells[12]=bash" +@SET "shells[13]=tclsh" +@SET "shells[14]=perl" +: +@SET "nextshell=13" +: +@rem asadmin is for automatic elevation to administrator. Separate window will be created (seems unavoidable with current elevation mechanism) and user will still get security prompt (probably reasonable). +: +@SET "asadmin=0" +: +@REM nextshell set to index for validshells .eg 10 for pwsh +@REM @ECHO nextshell is %nextshell% +@SET "selected=!shells[%nextshell%]!" +@REM @ECHO selected %selected% +@CALL SET "keyRemoved=%%validshells:'!selected!'=%%" +@REM @ECHO keyremoved %keyRemoved% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix but should be left in place) +@REM -- This section intended mainly to launch the next shell (and to escalate privileges if necessary) +@REM -- Avoid customising this if you are not familiar with batch scripting. cmd/batch script can be useful, but is probably the least expressive language and most error prone. +@REM -- For example - as this file needs to use unix-style lf line-endings - the label scanner is susceptible to the 512Byte boundary issue: https://www.dostips.com/forum/viewtopic.php?t=8988#p58888 +@REM -- This label issue can be triggered/abused in files with crlf line endings too - but it is less likely to happen accidentaly. +@REm -- See also: https://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/4095133#4095133 +@REM ############################################################################################################################ +@REM -- Due to this issue -seemingly trivial edits of the batch file section can break the script! (for Windows anyway) +@REM -- Even something as simple as adding or removing an @REM +@REM -- From within punkshell - use: +@REM -- deck scriptwrap.checkfile +@REM -- to check your templates or final wrapped scripts for byte boundary issues +@REM -- It will report any labels that are on boundaries +@REM -- This is why the nextshell value above is a 2 digit key instead of a string - so that editing the value doesn't change the byte offsets. +@REM -- Editing your sh,bash,tcl,pwsh payloads is much less likely to cause an issue. There is the possibility of the final batch :exit_multishell label spanning a boundary - so testing using deck scriptwrap.checkfile is still recommended. +@REM -- Alternatively, as you should do anyway - test the final script on windows +@REM -- Aside from adding comments/whitespace to tweak the location of labels - you can try duplicating the label (e.g just add the label on a line above) but this is not guaranteed to work in all situations. +@REM -- '@REM' is a safer comment mechanism than a leading colon - which is used sparingly here. +@REM -- A colon anywhere in the script that happens to land on a 512 Byte boundary (from file start or from a callsite) could be misinterpreted as a label +@REM -- It is unknown what versions of cmd interpreters behave this way - and deck scriptwrap.checkfile doesn't check all such boundaries. +@REm -- For this reason, batch labels should be chosen to be relatively unlikely to collide with other strings in the file, and simple names such as :exit or :end should probably be avoided +@REM ############################################################################################################################ +@REM -- custom windows payloads should be in powershell,tclsh (or sh/bash if available) code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SET "winpath=%~dp0" +@SET "fname=%~nx0" +@REM @ECHO fname %fname% +@REM @ECHO winpath %winpath% +@REM @ECHO commandlineascalled %0 +@REM @ECHO commandlineresolved %~f0 +@CALL :getNormalizedScriptTail nftail +@REM @ECHO normalizedscripttail %nftail% +@CALL :getFileTail %0 clinetail +@REM @ECHO clinetail %clinetail% +@CALL :stringToUpper %~nx0 capscripttail +@REM @ECHO capscriptname: %capscripttail% + +@IF "%nftail%"=="%capscripttail%" ( + @ECHO forcing asadmin=1 due to file name on filesystem being uppercase + @SET "asadmin=1" +) else ( + @CALL :stringToUpper %clinetail% capcmdlinetail + @REM @ECHO capcmdlinetail !capcmdlinetail! + IF "%clinetail%"=="!capcmdlinetail!" ( + @ECHO forcing asadmin=1 due to cmdline scriptname in uppercase + @set "asadmin=1" + ) +) +@SET "vbsGetPrivileges=%temp%\punk_bat_elevate_%fname%.vbs" +@SET arglist=%* +@SET "qstrippedargs=args%arglist%" +@SET "qstrippedargs=%qstrippedargs:"=%" +@IF "is%qstrippedargs:~4,13%"=="isPUNK-ELEVATED" ( + GOTO :gotPrivileges +) +@IF !asadmin!==1 ( + net file 1>NUL 2>NUL + @IF '!errorlevel!'=='0' ( GOTO :gotPrivileges ) else ( GOTO :getPrivileges ) +) +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@REM +@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 windows 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 "!shells[%nextshell%]!"=="pwsh" ( + REM pws 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; write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + REM fallback to powershell if pwsh failed + IF !pwshtest_exitcode!==0 ( + pwsh -nop -nol -c set-executionpolicy -Scope Process Unrestricted; "%~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM CALL powershell -nop -nol -c write-host powershell-found + REM powershell -nop -nol -file "%~dp0%~n0.ps1" %* + powershell -nop -nol -c set-executionpolicy -Scope Process Unrestricted; %~dp0%~n0.ps1" %arglist% + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF "!shells[%nextshell%]!"=="bash" ( + CALL :getWslPath %winpath% wslpath + REM ECHO wslfullpath "!wslpath!%fname%" + !shells[%nextshell%]! "!wslpath!%fname%" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + REM probably tclsh or sh + IF NOT "x%keyRemoved%"=="x%validshells%" ( + 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 + !shells[%nextshell%]! "%~dp0%fname%" %arglist% + SET task_exitcode=!errorlevel! + ) ELSE ( + ECHO %fname% has invalid nextshell value ^(%nextshell%^) !shells[%nextshell%]! valid options are %validshells% + SET task_exitcode=66 + @REM boundary padding + @REM boundary padding + GOTO :exit_multishell + ) + ) +) +@REM batch file library functions +@REM boundary padding +@GOTO :endlib + +:getWslPath +@SETLOCAL + @SET "_path=%~p1" + @SET "name=%~nx1" + @SET "drive=%~d1" + @SET "rtrn=%~2" + @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" +@ENDLOCAL & ( + @if "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + ECHO %result% + ) +) +@EXIT /B + +:getFileTail +@REM return tail of file without any normalization e.g c:/punkshell/bin/Punk.cmd returns Punk.cmd even if file is punk.cmd +@REM we can't use things such as %~nx1 as it can change capitalisation +@REM This function is designed explicitly to preserve capitalisation +@REM accepts full paths with either / or \ as delimiters - or +@SETLOCAL + @SET "rtrn=%~2" + @SET "arg=%~1" + @REM @SET "result=%_arg:*/=%" + @REM @SET "result=%~1" + @SET LF=^ + + + : The above 2 empty lines are important. Don't remove + @CALL :stringContains "!arg!" "\" hasBackSlash + @IF "!hasBackslash!"=="true" ( + @for %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:\=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @CALL :stringContains "!arg!" "/" hasForwardSlash + @IF "!hasForwardSlash!"=="true" ( + @FOR %%A in ("!LF!") do @( + @FOR /F %%B in ("!arg:/=%%~A!") do @set "result=%%B" + ) + ) ELSE ( + @set "result=%arg%" + ) + ) +@ENDLOCAL & ( + @if "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B +@REM boundary padding +@REM boundary padding +:getNormalizedScriptTail +@SETLOCAL + @SET "result=%~nx0" + @SET "rtrn=%~1" +@ENDLOCAL & ( + @IF "%~1" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO %result% + ) +) +@EXIT /B + +:getNormalizedFileTailFromPath +@REM warn via echo, and do not set return variable if path not found +@REM note that %~nx1 does not preserve case of provided path - hence the name 'normalized' +@REM boundary padding +@REM boundary padding +@REM boundary padding +@REM boundary padding +@SETLOCAL + @CALL :stringContains %~1 "\" hasBackSlash + @CALL :stringContains %~1 "/" hasForwardSlash + @IF "%hasBackslash%-%hasForwardslash%"=="false-false" ( + @SET "P=%cd%%~1" + @CALL :getNormalizedFileTailFromPath "!P!" ftail2 + @SET "result=!ftail2!" + ) else ( + @IF EXIST "%~1" ( + @SET "result=%~nx1" + ) else ( + @ECHO error getNormalizedFileTailFromPath file not found: %~1 + @EXIT /B 1 + ) + ) + @SET "rtrn=%~2" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + @ECHO getNormalizedFileTailFromPath %1 result: %result% + ) +) +@EXIT /B + +:stringContains +@REM usage: @CALL:stringContains string needle returnvarname +@SETLOCAL + @SET "rtrn=%~3" + @SET "string=%~1" + @SET "needle=%~2" + @IF "!string:%needle%=!"=="!string!" @( + @SET "result=false" + ) ELSE ( + @SET "result=true" + ) +@ENDLOCAL & ( + @IF "%~3" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringContains %string% %needle% result: %result% + ) +) +@EXIT /B + +:stringToUpper +@SETLOCAL + @SET "rtrn=%~2" + @SET "string=%~1" + @SET "capstring=%~1" + @FOR %%A in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) DO @( + @SET "capstring=!capstring:%%A=%%A!" + ) + @SET "result=!capstring!" +@ENDLOCAL & ( + @IF "%~2" neq "" ( + @SET "%rtrn%=%result%" + ) ELSE ( + @ECHO stringToUpper %string% result: %result% + ) +) +@EXIT /B + +: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 @SET taskexit_code=!errorlevel! & goto :exit_multishell +@GOTO :exit_multishell +# } +# -*- tcl -*- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- tcl script section +# -- This is a punk multishell file +# -- Primary payload target is Tcl, with sh,bash,powershell as helpers +# -- but it may equally be used with any of these being the primary script. +# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script +# -- i.e it is a polyglot file. +# -- The specific layout including some lines that appear just as comments is quite sensitive to change. +# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline. +# -- e.g ./filename.polypunk.cmd in sh or bash +# -- e.g tclsh filename.cmd +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +rename set ""; rename s set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup and restore +Hide :exit_multishell;Hide {<#};Hide '@ +namespace eval ::punk::multishell { + set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script [file dirname [file normalize [info script]/__]] + if {[info exists argv0] && + $last_script eq $last_script_root + } { + set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode + } else { + set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. + } + if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { + proc ::punk::multishell::is_main {{script_name {}}} { + if {$script_name eq ""} { + set script_name [file dirname [file normalize [info script]/--]] + } + if {![info exists ::punk::multishell::is_main($script_name)]} { + #e.g a .dll or something else unanticipated + puts stderr "Warning punk::multishell didn't recognize info script result: $script_name - will treat as if sourced and return instead of exiting" + puts stderr "Info: script_root: [file dirname [file normalize ${argv0}/__]]" + return 0 + } + return [set ::punk::multishell::is_main($script_name)] + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" +#puts "argv0 : $::argv0" +# -- --- --- --- --- --- --- --- --- --- --- --- + + +# +# + +# +# + + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- Best practice is to always return or exit above, or just by leaving the below defaults in place. +# -- If the multishell script is modified to have Tcl below the Tcl Payload section, +# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. +# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below +# -- but the sh/bash 'then' and 'fi' would also need to be uncommented. +# -- This facility left in place for experiments on whether configuration payloads etc can be appended +# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells +# -- can be made to ignore/cope with such data. +if {[::punk::multishell::is_main]} { + exit 0 +} else { + return +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +# end hide from unix shells \ +HEREDOC1B_HIDE_FROM_BASH_AND_SH +# sh/bash \ +shift && set -- "${@:1:$#-1}" +#------------------------------------------------------ +# -- This if block only needed if Tcl didn't exit or return above. +if false==false # else { + then + : # +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- sh/bash script section +# -- leave as is if all that is required is launching the Tcl payload" +# -- +# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default +# -- adjust the %nextshell% value above +# -- if sh/bash scripting needs to run on windows too. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +exitcode=0 +#printf "start of bash or sh code" + +# +# + +# -- --- --- --- --- --- --- --- +# +#-- 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 +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin perl Payload +my $exit_code = 0; +#use ExtUtils::Installed; +#my $installed = ExtUtils::Installed->new(); +#my @modules = $installed->modules(); +#print "Modules:\n"; +#foreach my $m (@modules) { +# print "$m\n"; +#} +# -- --- --- + + + +my $scriptname = $0; +print "perl $scriptname\n"; +my $i =1; +foreach my $a(@ARGV) { + print "Arg # $i: $a\n"; +} + +# +# + + + +# -- --- --- --- --- --- --- --- +# +$exit_code=system("tclsh", $scriptname, @ARGV); +#print "perl reporting tcl exitcode: $exit_code"; +# +# -- --- --- --- --- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end perl Payload +exit $exit_code; +__END__ + +# end hide sh/bash/perl block from Tcl +# This comment with closing brace should stay in place whether if commented or not } +#------------------------------------------------------ +# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above +if 0 { +: end heredoc1 - end hide from powershell \ +'@ +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- powershell/pwsh section +# -- Do not edit if current file is the .ps1 +# -- Edit the corresponding .cmd and it will autocopy +# -- unbalanced braces { } here *even in comments* will cause problems if there was no Tcl exit or return above +# -- custom script should generally go below the begin_powershell_payload line +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +function GetScriptName { $myInvocation.ScriptName } +$scriptname = GetScriptName +function GetDynamicParamDictionary { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$true, Mandatory=$true)] + [string] $CommandName + ) + + begin { + # Get a list of params that should be ignored (they're common to all advanced functions) + $CommonParameterNames = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject([type] [System.Management.Automation.Internal.CommonParameters]) | + Get-Member -MemberType Properties | + Select-Object -ExpandProperty Name + } + + process { + # Create the dictionary that this scriptblock will return: + $DynParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + + # Convert to object array and get rid of Common params: + (Get-Command $CommandName | select -exp Parameters).GetEnumerator() | + Where-Object { $CommonParameterNames -notcontains $_.Key } | + ForEach-Object { + $DynamicParameter = New-Object System.Management.Automation.RuntimeDefinedParameter ( + $_.Key, + $_.Value.ParameterType, + $_.Value.Attributes + ) + $DynParamDictionary.Add($_.Key, $DynamicParameter) + } + + # Return the dynamic parameters + return $DynParamDictionary + } +} +# GetDynamicParamDictionary +# - This can make it easier to share a single set of param definitions between functions +# - sample usage +#function ParameterDefinitions { +# param( +# [Parameter(Mandatory)][string] $myargument +# ) +#} +#function psmain { +# [CmdletBinding()] +# param() +# dynamicparam { GetDynamicParamDictionary ParameterDefinitions } +# process { +# #called once with $PSBoundParameters dictionary +# #can be used to validate arguments, or set a simpler variable name for access +# switch ($PSBoundParameters.keys) { +# 'myargumentname' { +# Set-Variable -Name $_ -Value $PSBoundParameters."$_" +# } +# #... +# } +# foreach ($boundparam in $PSBoundParameters.GetEnumerator()) { +# #... +# } +# } +# end { +# #Main function logic +# Write-Host "myargumentname value is: $myargumentname" +# #myotherfunction @PSBoundParameters +# } +#} +#psmain @args +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +#"Script Name : {0}" -f $scriptname | write-host +#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +#"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- +# +tclsh $scriptname $args +#"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/vfs/_vfscommon.vfs/modules/punk/pdf-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/pdf-0.1.0.tm index 15767ef5..1ee63f53 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/pdf-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/pdf-0.1.0.tm @@ -1894,6 +1894,7 @@ tcl::namespace::eval punk::pdf::lib { dict set map %marker% "[punk::ansi::a bold cyan]MERGEDBLOCK[punk::ansi::a]" puts $outc [string map $map $opt_blocksep] } + set blockresult "" if {$opt_shrink_textfree_blocks} { set teststripped [punk::ansi::ansistrip $mergedblock] if {[string trim $teststripped] ne ""} { diff --git a/src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm index 87863f88..2ed4f1e4 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/zip-0.1.1.tm @@ -400,7 +400,7 @@ tcl::namespace::eval punk::zip { #todo - if it was internal preamble - need to adjust offsets to fix the split off zipfile set zout [open $outfile_zip w] fconfigure $zout -encoding iso8859-1 -translation binary - chan copy $inzip $zout + chan copy $inzip $zout close $zout } close $inzip