From 86c612704bed9ad9488532f33914be2b098382f0 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 17 Aug 2023 07:49:23 +1000 Subject: [PATCH] A late checkin of much functionality. Tcl du cmd, Multishell cross-platform shell script and pmix wrap, make.tcl fixes & changes --- .gitignore | 2 + src/make.tcl | 233 +- src/modules/flagfilter-0.3.tm | 1 + src/modules/punk-0.1.tm | 160 +- src/modules/punk/config-0.1.tm | 10 + src/modules/punk/mix-0.2.tm | 1149 +++++++--- src/modules/punk/mix/base-0.1.tm | 9 +- .../project/.fossil-settings/empty-dirs | 6 +- .../templates/layouts/project/src/README.md | 2 +- .../layouts/project/src/embedded/README.md | 5 + .../layouts/project/src/lib/README.md | 7 + .../templates/layouts/project/src/make.tcl | 54 +- .../layouts/project/src/modules/README.md | 11 + .../layouts/project/src/runtime/Readme.md | 3 + .../layouts/project/src/scriptapps/README.md | 20 + .../layouts/project/src/vendorlib/README.md | 8 + .../project/src/vendormodules/README.md | 7 + .../punk/mix/templates/utility/multishell.cmd | 264 +++ .../punk/mix/templates/utility/shellbat.txt | 210 +- .../mix/templates/utility/shellbat_v1.txt | 106 + src/modules/punk/repo-999999.0a1.0.tm | 420 +++- src/modules/punk/winpath-999999.0a1.0.tm | 4 +- src/scriptapps/dtplite.tcl | 28 + src/{ => scriptapps}/fetchruntime.ps1 | 0 src/{deps => vendormodules}/dictutils-0.2.tm | 0 src/{deps => vendormodules}/metaface-1.2.5.tm | 0 src/vendormodules/natsort-0.1.1.5.tm | 1883 +++++++++++++++++ src/vendormodules/oolib-0.1.tm | 195 ++ src/{deps => vendormodules}/pattern-1.2.4.tm | 0 .../patterncmd-1.2.4.tm | 0 .../patternlib-1.2.6.tm | 0 .../patternpredator2-1.2.4.tm | 0 32 files changed, 4269 insertions(+), 528 deletions(-) create mode 100644 src/modules/punk/mix/templates/layouts/project/src/embedded/README.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/lib/README.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/modules/README.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/runtime/Readme.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/scriptapps/README.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/vendorlib/README.md create mode 100644 src/modules/punk/mix/templates/layouts/project/src/vendormodules/README.md create mode 100644 src/modules/punk/mix/templates/utility/multishell.cmd create mode 100644 src/modules/punk/mix/templates/utility/shellbat_v1.txt create mode 100644 src/scriptapps/dtplite.tcl rename src/{ => scriptapps}/fetchruntime.ps1 (100%) rename src/{deps => vendormodules}/dictutils-0.2.tm (100%) rename src/{deps => vendormodules}/metaface-1.2.5.tm (100%) create mode 100644 src/vendormodules/natsort-0.1.1.5.tm create mode 100644 src/vendormodules/oolib-0.1.tm rename src/{deps => vendormodules}/pattern-1.2.4.tm (100%) rename src/{deps => vendormodules}/patterncmd-1.2.4.tm (100%) rename src/{deps => vendormodules}/patternlib-1.2.6.tm (100%) rename src/{deps => vendormodules}/patternpredator2-1.2.4.tm (100%) diff --git a/.gitignore b/.gitignore index ca6d7864..5833b180 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,7 @@ _FOSSIL_ #miscellaneous editor files etc *.swp +/src/modules/punk/mix/templates/utility/multishell.ps1 + todo.txt diff --git a/src/make.tcl b/src/make.tcl index c63af6d2..70b41b1b 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -5,32 +5,199 @@ #It is assumed the src folder has been placed somewhere where appropriate #(e.g not in /usr or c:/ - unless you intend it to directly make and place folders and files in those locations) -package require punk::mix +set hashline "# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ###" +puts $hashline +puts " punkshell make script " +puts $hashline\n +namespace eval ::punkmake { + variable scriptfolder [file normalize [file dirname [info script]]] + variable foldername [file tail $scriptfolder] + variable pkg_requirements [list]; variable pkg_missing [list];variable pkg_loaded [list] + variable non_help_flags [list -k] + variable help_flags [list -help --help /?] + variable known_commands [list project get-project-info] +} +if {"::try" ni [info commands ::try]} { + puts stderr "Tcl interpreter possibly too old - 'try' command not found - aborting" + exit 1 +} +# ** *** *** *** *** *** *** *** *** *** *** *** +#*temporarily* hijack package command +# ** *** *** *** *** *** *** *** *** *** *** *** +try { + rename ::package ::punkmake::package_temp_aside + proc ::package {args} { + if {[lindex $args 0] eq "require"} { + lappend ::punkmake::pkg_requirements [lindex $args 1] + } + } + package require punk::mix + package require punk::repo +} finally { + catch {rename ::package ""} + catch {rename ::punkmake::package_temp_aside ::package} +} +# ** *** *** *** *** *** *** *** *** *** *** *** +foreach pkg $::punkmake::pkg_requirements { + if {[catch {package require $pkg} errM]} { + puts stderr "missing pkg: $pkg" + lappend ::punkmake::pkg_missing $pkg + } else { + lappend ::punkmake::pkg_loaded $pkg + } +} + + + + +proc punkmake_gethelp {args} { + set scriptname [file tail [info script]] + append h "Usage:" \n + append h "" \n + append h " $scriptname -help or $scriptname --help or $scriptname /? or just $scriptname" \n + append h " - This help." \n \n + append h " $scriptname project ?-k?" \n + append h " - this is the literal word project - and confirms you want to run the project build" \n + append h " - the optional -k flag will terminate processes running as the executable being built (if applicable)" \n \n + append h " $scriptname get-project-info" \n + append h " - show the name and base folder of the project to be built" \n + append h "" \n + if {[llength $::punkmake::pkg_missing]} { + append h "* ** NOTE ** ***" \n + append h " punkmake has detected that the following packages could not be loaded:" \n + append h " " [join $::punkmake::pkg_missing "\n "] \n + append h "* ** *** *** ***" \n + append h " These packages are required for punk make to function" \n \n + append h "* ** *** *** ***" \n\n + append h "Successfully Loaded packages:" \n + append h " " [join $::punkmake::pkg_loaded "\n "] \n + } + return $h +} +set scriptargs $::argv +set do_help 0 +if {![llength $scriptargs]} { + set do_help 1 +} else { + foreach h $::punkmake::help_flags { + if {[lsearch $scriptargs $h] >= 0} { + set do_help 1 + break + } + } +} +set commands_found [list] +foreach a $scriptargs { + if {![string match -* $a]} { + lappend commands_found $a + } else { + if {$a ni $::punkmake::non_help_flags} { + set do_help 1 + } + } +} +if {[llength $commands_found] != 1 } { + set do_help 1 +} elseif {[lindex $commands_found 0] ni $::punkmake::known_commands} { + puts stderr "Unknown command: [lindex $commands_found 0]\n\n" + set do_help 1 +} +if {$do_help} { + puts stderr [punkmake_gethelp] + exit 1 +} + +set ::punkmake::command [lindex $commands_found 0] + if {[lsearch $::argv -k] >= 0} { set forcekill 1 } else { set forcekill 0 } -puts stdout "::argv $::argv" -set sourcefolder [file normalize [file dirname [info script]]] +#puts stdout "::argv $::argv" # ---------------------------------------- -set target_modules_base [file dirname $sourcefolder]/modules -file mkdir $target_modules_base +set scriptfolder $::punkmake::scriptfolder + -#external modules first - and any supporting files - no 'building' required -set copied [punk::mix::cli::lib::copy_files_from_source_to_base $sourcefolder/deps $target_modules_base -force 1] -puts stderr "Copied [llength $copied] dependencies" +#first look for a project root (something under fossil or git revision control AND matches punk project folder structure) +#If that fails - just look for a 'project shaped folder' ie meets minimum requirements of /src /src/lib /src/modules /lib /modules +if {![string length [set projectroot [punk::repo::find_project $scriptfolder]]]} { + if {![string length [set projectroot [punk::repo::find_candidate $scriptfolder]]]} { + puts stderr "punkmake script unable to determine an approprite project root at or above the path '$scriptfolder' ensure the make script is within a project folder structure" + puts stderr " -aborted- " + exit 2 + #todo? + #ask user for a project name and create basic structure? + #call punk::mix::cli::new $projectname on parent folder? + } else { + puts stderr "WARNING punkmake script operating in project space that is not under version control" + } +} else { + +} + +if {$::punkmake::command eq "get-project-info"} { + puts stdout "- -- --- --- --- --- --- --- --- --- ---" + puts stdout "- -- get-project-info -- -" + puts stdout "- -- --- --- --- --- --- --- --- --- ---" + puts stdout "- projectroot : $projectroot" + if {[punk::repo::find_fossil $scriptfolder] eq $projectroot} { + set vc "fossil" + set rev [punk::repo::fossil_revision $scriptfolder] + } elseif {[punk::repo::find_git $scriptfolder] eq $projectroot} { + set vc "git" + set rev [punk::repo::git_revision $scriptfolder] + } else { + set vc " - none found -" + set rev "n/a" + } + puts stdout "- version control : $vc" + puts stdout "- revision : $rev" + puts stdout "- -- --- --- --- --- --- --- --- --- ---" + + exit 0 +} + +if {$::punkmake::command ne "project"} { + puts stderr "Command $::punkmake::command not implemented - aborting." + exit 1 +} -set src_module_dir $sourcefolder/modules -#modules and associated files belonging to this package/app -set copied [punk::mix::cli::lib::build_modules_from_source_to_base $src_module_dir $target_modules_base -glob *.tm] ;#will only accept a glob ending in .tm -puts stderr "Copied [llength $copied] app modules" -set copied [punk::mix::cli::lib::copy_nonmodules_from_source_to_base $src_module_dir $target_modules_base -force 1] +set sourcefolder $projectroot/src +#only a single consolidated /modules folder used for target +set target_modules_base $projectroot/modules +file mkdir $target_modules_base + +#external libs and modules first - and any supporting files - no 'building' required +if {[file exists $sourcefolder/vendorlib]} { + set copied [punk::mix::cli::lib::copy_files_from_source_to_target $sourcefolder/vendorlib $projectroot/lib -overwrite ALL-TARGETS] + puts stderr "Copied [llength $copied] vendor libs from src/vendorlib to $projectroot/lib" +} else { + puts stderr "NOTE: No src/vendorlib folder found." +} +if {[file exists $sourcefolder/vendormodules]} { + set copied [punk::mix::cli::lib::copy_files_from_source_to_target $sourcefolder/vendormodules $target_modules_base -overwrite ALL-TARGETS] + puts stderr "Copied [llength $copied] vendor modules from src/vendormodules to $target_modules_base" +} else { + puts stderr "NOTE: No src/vendormodules folder found." +} + +#default source module folder is at projectroot/src/modules +#There may be multiple other src module folders at same level (e.g folder not being other special-purpose folder and not matching name vendor* that contains at least one .tm file in its root) +set source_module_folderlist [punk::mix::cli::lib::find_source_module_paths $projectroot] +foreach src_module_dir $source_module_folderlist { + set dirtail [file tail $src_module_dir] + #modules and associated files belonging to this package/app + set copied [punk::mix::cli::lib::build_modules_from_source_to_base $src_module_dir $target_modules_base -glob *.tm] ;#will only accept a glob ending in .tm + puts stderr "Copied [llength $copied] tm modules from src/$dirtail to $target_modules_base " + + set copied [punk::mix::cli::lib::copy_nonmodules_from_source_to_base $src_module_dir $target_modules_base -overwrite ALL-TARGETS] +} # ---------------------------------------- @@ -40,7 +207,14 @@ if {![llength $vfs_folders]} { puts stdout " -done- " exit 0 } -file mkdir $sourcefolder/_build + +set buildfolder [punk::mix::cli::lib::get_build_folder $sourcefolder] +if {$buildfolder ne "$sourcefolder/_build"} { + puts stderr "$sourcefolder/_build doesn't match the project buildfolder $buildfolder - check project filestructure" + puts stdout " -aborted- " + exit 2 +} + #find runtime - only supports one for now.. REVIEW set rtfolder $sourcefolder/runtime @@ -65,27 +239,28 @@ if {[llength $runtimes] > 1} { set runtimefile [lindex $runtimes 0] #sdx *may* be pointed to use the runtime we use to build the kit, or the user may manually use this runtime if they don't have tclsh #sdx will complain if the same runtime is used for the shell as is used in the -runtime argument - so we make a copy (REVIEW) -if {![file exists $sourcefolder/_build/buildruntime.exe]} { - file copy $rtfolder/$runtimefile $sourcefolder/_build/buildruntime.exe +if {![file exists $buildfolder/buildruntime.exe]} { + file copy $rtfolder/$runtimefile $buildfolder/buildruntime.exe } - +set startdir [pwd] puts stdout "Found [llength $vfs_folders] .vfs folders - building executable for each..." +cd [file dirname $buildfolder] foreach vfs $vfs_folders { set vfsname [file rootname $vfs] puts stdout " Processing vfs $sourcefolder/$vfs" puts stdout " ------------------------------------" - if {[file exists $sourcefolder/_build/$vfsname]} { - puts stderr "deleting existing $sourcefolder/_build/$vfsname" + if {[file exists $buildfolder/$vfsname]} { + puts stderr "deleting existing $buildfolder/$vfsname" file delete $sourcefolder/_build/$vfsname } puts stdout "building $vfsname with sdx.. vfsdir:$vfs cwd: [pwd]" if {[catch { - exec sdx wrap [pwd]/_build/$vfsname -vfs [pwd]/$vfs -runtime $sourcefolder/_build/buildruntime.exe -verbose + exec sdx wrap $buildfolder/$vfsname -vfs $sourcefolder/$vfs -runtime $buildfolder/buildruntime.exe -verbose } result]} { - puts stderr "sdx wrap _build/$vfsname -vfs [pwd]/$vfs -runtime $sourcefolder/_build/buildruntime.exe -verbose failed with msg: $result" + puts stderr "sdx wrap _build/$vfsname -vfs $sourcefolder/$vfs -runtime $buildfolder/buildruntime.exe -verbose failed with msg: $result" } else { puts stdout "ok - finished sdx" set separator [string repeat = 40] @@ -94,7 +269,7 @@ foreach vfs $vfs_folders { puts stdout $separator } - if {![file exists $sourcefolder/_build/$vfsname]} { + if {![file exists $buildfolder/$vfsname]} { puts stderr "|err> build didn't seem to produce output at $sourcefolder/_build/$vfsname" exit 2 } @@ -158,18 +333,18 @@ foreach vfs $vfs_folders { set targetexe $vfsname } - if {[file exists $sourcefolder/_build/$targetexe]} { - puts stderr "deleting existing $sourcefolder/_build/$targetexe" + if {[file exists $buildfolder/$targetexe]} { + puts stderr "deleting existing $buildfolder/$targetexe" if {[catch { file delete $sourcefolder/_build/$targetexe } msg]} { - puts stderr "Failed to delete $sourcefolder/_build/$targetexe" + puts stderr "Failed to delete $buildfolder/$targetexe" exit 4 } } if {$::tcl_platform(platform) eq "windows"} { - file rename $sourcefolder/_build/$vfsname $sourcefolder/_build/${vfsname}.exe + file rename $buildfolder/$vfsname $sourcefolder/_build/${vfsname}.exe } after 200 @@ -189,13 +364,13 @@ foreach vfs $vfs_folders { puts stdout "copying.." - puts stdout "$sourcefolder/_build/$targetexe" + puts stdout "$buildfolder/$targetexe" puts stdout "to:" puts stdout "$deployment_folder/$targetexe" after 500 - file copy $sourcefolder/_build/$targetexe $deployment_folder/$targetexe + file copy $buildfolder/$targetexe $deployment_folder/$targetexe } - +cd $startdir puts stdout "done" exit 0 diff --git a/src/modules/flagfilter-0.3.tm b/src/modules/flagfilter-0.3.tm index df61a724..19bbfaf1 100644 --- a/src/modules/flagfilter-0.3.tm +++ b/src/modules/flagfilter-0.3.tm @@ -624,6 +624,7 @@ namespace eval flagfilter { set o_values $values set o_remaining [lsearch -all $values *] ;#create a list of indices e.g 0 1 2 3 4 5 6 set o_allocated [list] + set o_map [list] foreach posn $o_remaining { lappend o_map $posn unallocated } diff --git a/src/modules/punk-0.1.tm b/src/modules/punk-0.1.tm index 08fabe76..08ea9014 100644 --- a/src/modules/punk-0.1.tm +++ b/src/modules/punk-0.1.tm @@ -135,7 +135,44 @@ namespace eval punk { set the_var } } - + proc src {args} { + #based on wiki.. https://wiki.tcl-lang.org/page/source+with+args + #added support for ?-encoding name? and other options of Tcl source command under assumption they come pairs before the filename + # review? seems unlikely source command will ever accept solo options. It would make complete disambiguation impossible when passing additional args as we are doing here. + set cmdargs [list] + set scriptargs [list] + set inopts 0 + set i 0 + foreach a $args { + if {$i eq [llength $args]-1} { + #reached end without finding end of opts + #must be file - even if it does match -* ? + break + } + if {!$inopts} { + if {[string match -* $a]} { + set inopts 1 + } else { + #leave loop at first nonoption - i should be index of file + break + } + } else { + #leave for next iteration to check + set inopts 0 + } + incr i + } + set cmdargs [lrange $args 0 $i] + set scriptargs [lrange $args $i+1 end] + set argv $::argv + set argc $::argc + set ::argv $scriptargs + set ::argc [llength $scriptargs] + set code [catch {uplevel [list source {*}$cmdargs]} return] + set ::argv $argv + set ::argc $argc + return -code $code $return + } #https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/ # #we can't provide a float comparison suitable for every situation, @@ -6803,130 +6840,11 @@ namespace eval punk { interp alias {} tmhere {} .= pwd |path> {::tcl::tm::add {*}$data; set path} |> inspect -label added_to_module_path <0/#| - proc norm {path} { - #kettle::path::norm - #see also wiki - #full path normalization - return [file dirname [file normalize $path/__]] - } - - proc path_strip_prefix {path prefix} { - return [file join \ - {*}[lrange \ - [file split [norm $path]] \ - [llength [file split [norm $prefix]]] \ - end]] - } - - proc path_relative {base dst} { - # Modified copy of ::fileutil::relative (tcllib) - # Adapted to 8.5 ({*}). - # - # Taking two _directory_ paths, a base and a destination, computes the path - # of the destination relative to the base. - # - # Arguments: - # base The path to make the destination relative to. - # dst The destination path - # - # Results: - # The path of the destination, relative to the base. - - # Ensure that the link to directory 'dst' is properly done relative to - # the directory 'base'. - - if {[file pathtype $base] ne [file pathtype $dst]} { - return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)" - } - - set base [norm $base] - set dst [norm $dst] - - set save $dst - set base [file split $base] - set dst [file split $dst] - - while {[lindex $dst 0] eq [lindex $base 0]} { - set dst [lrange $dst 1 end] - set base [lrange $base 1 end] - if {![llength $dst]} {break} - } - - set dstlen [llength $dst] - set baselen [llength $base] - if {($dstlen == 0) && ($baselen == 0)} { - # Cases: - # (a) base == dst - set dst . - } else { - # Cases: - # (b) base is: base/sub = sub - # dst is: base = {} - - # (c) base is: base = {} - # dst is: base/sub = sub - - while {$baselen > 0} { - set dst [linsert $dst 0 ..] - incr baselen -1 - } - set dst [file join {*}$dst] - } - - return $dst - } - proc fcat {args} { - if {$::tcl_platform(platform) ne "windows"} { - return [fileutil::cat {*}$args] - } - - set knownopts [list -eofchar -translation -encoding --] - set last_opt 0 - for {set i 0} {$i < [llength $args]} {incr i} { - set ival [lindex $args $i] - #puts stdout "i:$i a: $ival known: [expr {$ival in $knownopts}]" - if {$ival eq "--"} { - set last_opt $i - break - } else { - if {$ival in $knownopts} { - #puts ">known at $i : [lindex $args $i]" - if {($i % 2) != 0} { - error "unexpected option at index $i. known options: $knownopts must come in -opt val pairs." - } - incr i - set last_opt $i - } else { - set last_opt [expr {$i - 1}] - break - } - } - } - set first_non_opt [expr {$last_opt + 1}] - - #puts stderr "first_non_opt: $first_non_opt" - set opts [lrange $args -1 $first_non_opt-1] - set paths [lrange $args $first_non_opt end] - if {![llength $paths]} { - error "Unable to find file in the supplied arguments: $args. Ensure options are all -opt val pairs and that file name(s) follow" - } - #puts stderr "opts: $opts paths: $paths" - set finalpaths [list] - foreach p $paths { - if {[punk::winpath::illegalname_test $p]} { - lappend finalpaths [punk::winpath::illegalname_fix $p] - } else { - lappend finalpaths $p - } - } - fileutil::cat {*}$opts {*}$finalpaths - } - #simplify path with respect to /./ & /../ elements - independent of platform #NOTE: anomalies in standard tcl processing on windows: #e.g file normalize {//host} -> c:/host @@ -6959,7 +6877,7 @@ namespace eval punk { } #fileutil::cat except with checking for windows illegal path names (when on windows platform) - interp alias {} fcat {} punk::fcat + interp alias {} fcat {} punk::repo::fcat #---------------------------------------------- interp alias {} linelistraw {} punk::linelistraw diff --git a/src/modules/punk/config-0.1.tm b/src/modules/punk/config-0.1.tm index 1ad3f45e..e034d6b7 100644 --- a/src/modules/punk/config-0.1.tm +++ b/src/modules/punk/config-0.1.tm @@ -14,6 +14,8 @@ namespace eval punk::config { variable vars set vars [list \ apps \ + config \ + configset \ scriptlib \ color_stdout \ color_stderr \ @@ -21,17 +23,20 @@ namespace eval punk::config { logfile_stderr \ syslog_stdout \ syslog_stderr \ + syslog_active \ exec_unknown \ ] #todo pkg punk::config #defaults + dict set startup configset .punkshell dict set startup exec_unknown true ;#whether to use exec instead of experimental shellfilter::run dict set startup color_stdout [list cyan bold] dict set startup color_stderr [list red bold] dict set startup syslog_stdout "127.0.0.1:514" dict set startup syslog_stderr "127.0.0.1:514" + dict set startup syslog_active 0 #default file logs to logs folder at same location as exe if writable, or empty string dict set startup logfile_stdout "" dict set startup logfile_stderr "" @@ -54,6 +59,8 @@ namespace eval punk::config { #todo - define which configvars are settable in env set known_punk_env_vars [list \ PUNK_APPS \ + PUNK_CONFIG \ + PUNK_CONFIGSET \ PUNK_SCRIPTLIB \ PUNK_EXECUNKNOWN \ PUNK_COLOR_STDERR \ @@ -62,6 +69,7 @@ namespace eval punk::config { PUNK_LOGFILE_STDERR \ PUNK_SYSLOG_STDOUT \ PUNK_SYSLOG_STDERR \ + PUNK_SYSLOG_ACTIVE \ ] #override with env vars if set @@ -81,4 +89,6 @@ namespace eval punk::config { set running [dict create] set running [dict merge $running $startup] + + } diff --git a/src/modules/punk/mix-0.2.tm b/src/modules/punk/mix-0.2.tm index 3ebb0b99..fdf859d9 100644 --- a/src/modules/punk/mix-0.2.tm +++ b/src/modules/punk/mix-0.2.tm @@ -4,6 +4,8 @@ package provide punk::mix [namespace eval punk::mix { }] +package require punk::repo + namespace eval punk::mix::cli { namespace ensemble create @@ -14,8 +16,65 @@ namespace eval punk::mix::cli { return $basehelp } + proc status {{project ""}} { + set result "" + if {[string length $project]} { + puts stderr "project status unimplemented" + return + } + set active_dir [pwd] + + if {[punk::repo::is_fossil $active_dir]} { + set fosroot [punk::repo::find_fossil $active_dir] + if {[punk::repo::is_candidate_root $active_dir] && ([string tolower $fosroot] ne [string tolower $active_dir])} { + + append result "**" \n + append result "** current folder has /src & /modules dirs - but isn't the project root" \n + append result "** current folder: $active_dir" \n + append result "** project root : $fosroot ([punk::repo::path_relative $active_dir $fosroot])" \n + append result "**" \n + + } + append result "FOSSIL project based at $fosroot with revision: [punk::repo::fossil_revision $active_dir]" \n + set dbinfo [exec fossil dbstat] + append result [join [punk::repo::grep {project-name:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {tickets:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {project-age:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {latest-change:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {files:*} $dbinfo] \n] \n + append result [join [punk::repo::grep {check-ins:*} $dbinfo] \n] \n + set timeline [exec fossil timeline -n 5 -t ci] + set timeline [string map [list \r\n \n] $timeline] + append result $timeline + } else { + append result "Not a punk fossil project" \n + if {[punk::repo::is_git $active_dir]} { + append result "GIT project based at [punk::repo::find_git $active_dir] with revision: [punk::repo::git_revision $active_dir]" \n + } else { + append result "No repository located for current folder $active_dir" \n + if {[string length [set candidate [punk::repo::find_candidate $active_dir]]]} { + append result "Candidate project root found at : $candidate" \n + append result " - consider putting this folder under fossil control (and/or git)" \n + } else { + append result "No candidate project root found. Expecting folder containing src,src/lib,src/modules,lib,modules" \n + } + } + } + return $result + } + proc tickets {{project ""}} { + set result "" + if {[string length $project]} { + puts stderr "project status unimplemented" + return + } + set active_dir [pwd] + append result "Retrieving top 10 tickets only (for more, use fossil timeline -n -t t)" \n + append result [exec fossil timeline -n 10 -t t] - proc fossilize {projectname args} { + return $result + } + proc fossilize { args} { #check if project already managed by fossil.. initialise and check in if not. puts stderr "unimplemented" } @@ -46,15 +105,19 @@ namespace eval punk::mix::cli { set opt_confirm [string tolower [dict get $opts -confirm]] set startdir [pwd] - if {[lib::is_project_dir $startdir]} { + if {[punk::repo::is_project $startdir]} { puts stderr "Already in a project directory '$startdir' - move to a base location suitable for a new project" + puts stderr " todo: pmix newsubproject" return } - + set projectdir $startdir/$projectname + + set tpldir [lib::mix_templates_dir] if {[file exists $projectdir] && !($opt_force || $opt_update)} { - error "Unable to create new project at $projectdir - file/folder already exists use -update 1 to fill in missing items from template use -force 1 to overwrite from template" + puts stderr "Unable to create new project at $projectdir - file/folder already exists use -update 1 to fill in missing items from template use -force 1 to overwrite from template" + return } elseif {[file exists $projectdir] && $opt_force} { puts stderr "mix new WARNING: -force 1 was supplied. Will copy layout $tpldir/layouts/$opt_layout using -force option to overwrite from template" if {$opt_confirm ni [list 0 no false]} { @@ -64,7 +127,7 @@ namespace eval punk::mix::cli { set answer [gets stdin] fconfigure stdin -blocking [dict get $stdin_state -blocking] if {$answer ne "Y"} { - puts stderr "mix new aborting due to user response '$answer' (required Y to procedd) use -confirm 0 to avoid prompts." + puts stderr "mix new aborting due to user response '$answer' (required Y to proceed) use -confirm 0 to avoid prompts." return } } @@ -73,31 +136,66 @@ namespace eval punk::mix::cli { } - #todo - lookup config for .fossil repo location. For now use current dir. - if {![file exists $startdir/$projectname.fossil]} { - puts stdout "Initialising fossil repo: $startdir/$projectname.fossil" - set fossilinit [runx -n fossil init $projectname.fossil -project-name $projectname] - if {[dict get $fossilinit exitcode] != 0} { - puts stderr "fossil init failed:" - puts stderr [dict get $fossilinit stderr] - return - } else { - puts stdout "fossil init result:" - puts stdout [dict get $fossilinit stdout] - } - } else { + if {[file exists $startdir/$projectname.fossil]} { puts stdout "NOTICE: $startdir/$projectname.fossil already exists" if {!($opt_force || $opt_update)} { + puts stderr "-force 1 or -update 1 not specified - aborting" return } } + + #todo - lookup config for .fossil repo location. For now use current dir. + + if {[punk::repo::is_git $startdir]} { + puts stderr "mix new WARNING: you are already within a git repo based at [punk::repo::find_git $startdir]" + puts stderr "The new project will create a fossil repository (which you are free to ignore - but but will be used to confirm project base)" + puts stderr "If you intend to use both git and fossil in the same project space - you should research and understand the details and any possible interactions/issues" + puts stdout "Do you want to proceed to create a project based at: $projectdir? Y|N" + set stdin_state [fconfigure stdin] + fconfigure stdin -blocking 1 + set answer [gets stdin] + fconfigure stdin -blocking [dict get $stdin_state -blocking] + if {$answer ne "Y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y to proceed) use -confirm 0 to avoid prompts." + return + } + } + set is_nested_fossil 0 ;#default assumption + if {[punk::repo::is_fossil $startdir]} { + puts stderr "mix new WARNING: you are already within an open fossil repo based at [punk::repo::find_fossil $startdir] NESTED fossil repository" + if {$opt_confirm ni [list 0 no false]} { + puts stderr "If you proceed - the new project's fossil repo will be created using the --nested flag" + puts stdout "Do you want to proceed to create a NESTED project based at: $projectdir? Y|N" + set stdin_state [fconfigure stdin] + fconfigure stdin -blocking 1 + set answer [gets stdin] + fconfigure stdin -blocking [dict get $stdin_state -blocking] + if {$answer ne "Y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y to proceed) use -confirm 0 to avoid prompts." + return + } + set is_nested_fossil 1 + } + } + + puts stdout "Initialising fossil repo: $startdir/$projectname.fossil" + set fossilinit [runx -n fossil init $projectname.fossil -project-name $projectname] + if {[dict get $fossilinit exitcode] != 0} { + puts stderr "fossil init failed:" + puts stderr [dict get $fossilinit stderr] + return + } else { + puts stdout "fossil init result:" + puts stdout [dict get $fossilinit stdout] + } + file mkdir $projectdir set layout_dir $tpldir/layouts/$opt_layout if {$opt_force} { - lib::copy_files_from_source_to_base $layout_dir $projectdir -force $opt_force + lib::copy_files_from_source_to_target $layout_dir $projectdir -overwrite ALL-TARGETS #file copy -force $layout_dir $projectdir } else { - lib::copy_files_from_source_to_base $layout_dir $projectdir + lib::copy_files_from_source_to_target $layout_dir $projectdir } #expect this in all templates? - todo make these substitutions independent of specific paths and filenames? @@ -123,9 +221,14 @@ namespace eval punk::mix::cli { Kettle doc cd $projectdir - if {![file exists $projectdir/_FOSSIL_]} { + if {![punk::repo::is_fossil_root $projectdir]} { set first_fossil 1 - set fossilopen [runx -n fossil open ../$projectname.fossil -k] + #-k = keep. (only modify the manifest file(s)) + if {$is_nested_fossil} { + set fossilopen [runx -n fossil open --nested ../$projectname.fossil -k] + } else { + set fossilopen [runx -n fossil open ../$projectname.fossil -k] + } if {[dict get $fossilopen exitcode] != 0} { puts stderr "fossil open in project workdir '$projectdir' FAILED:" puts stderr [dict get $fossilopen stderr] @@ -159,8 +262,141 @@ namespace eval punk::mix::cli { puts stdout "-done- project:$projectname projectdir: $projectdir" } - interp alias {} ::punk::mix::cli::project {} ::punk::mix::cli::new - + interp alias {} ::punk::mix::cli::newproject {} ::punk::mix::cli::new + + proc wrap_in_multishell {filepath args} { + set defaults [list -askme 1] + set opts [dict merge $defaults $args] + + set opt_askme [dict get $opts -askme] + + if {[file type $filepath] ne "file"} { + error "wrap_in_multishell: only script files can be wrapped." + } + set ext [string trim [file extension $filepath] .] + #set allowed_extensions [list tcl ps1 sh bash] + #TODO + set allowed_extensions [list tcl] + if {[string tolower $ext] ni $allowed_extensions} { + error "wrap_in_multishell: script must have file extension in list: $allowed_extensions" + } + + set output_file [file rootname $filepath].cmd + if {[file exists $output_file]} { + error "wrap_in_multishell: target file $output_file already exists.. aborting" + } + + + set startdir [pwd] + set workroot [punk::repo::find_candidate $startdir] + set wrapper_template $workroot/src/ + + set tpldir [lib::mix_templates_dir] + set wrapper_template $tpldir/utility/multishell.cmd + if {![file exists $wrapper_template]} { + error "wrap_in_multishell: unable to find multishell template at $wrapper_template" + } + set fdt [open $wrapper_template r] + fconfigure $fdt -translation binary + set template_data [read $fdt] + close $fdt + puts stdout "Read [string length $template_data] bytes of template data.." + set template_lines [split $template_data \n] + puts stdout "Displaying first 3 lines of template between dashed lines..." + puts stdout "-----------------------------------------------" + foreach ln [lrange $template_lines 0 3] { + puts stdout $ln + } + puts stdout "-----------------------------------------------\n" + #foreach ln $template_lines { + #} + + 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" + if {$opt_askme} { + puts stdout "Target for above data is '$output_file'" + puts stdout "Does this look correct? Y|N" + set stdin_state [fconfigure stdin] + fconfigure stdin -blocking 1 + set answer [gets stdin] + if {[string tolower $answer] ne "y"} { + fconfigure stdin -blocking [dict get $stdin_state -blocking] + + puts stderr "mix new aborting due to user response '$answer' (required Y or y to proceed) use -askme 0 to avoid prompts." + return + } + fconfigure stdin -blocking [dict get $stdin_state -blocking] + } + + set start_idx 0 + set end_idx 0 + set line_idx 0 + set existing_payload [list] + foreach ln $template_lines { + + if {[string match "#*" $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] + } + } else { + + } + incr line_idx + } + if {($start_idx == 0) || ($end_idx == 0)} { + error "wrap_in_multishell was unable to find payload area in template marked with # and # on separate lines" + } + set existing_string [join $existing_payload \n] + if {[string length [string trim $existing_string]]} { + puts stdout "EXISTING PAYLOAD!!" + puts stdout "-----------------------------------------------\n" + puts stdout $existing_string + puts stdout "-----------------------------------------------\n" + error "wrap_in_multishell found existing payload.. aborting." + #todo - allow overwrite only in files outside of punkshell distribution? + if 0 { + puts stderr "Found existing payload.. overwrite?" + if {$opt_askme} { + puts stdout "Are you sure you want to replace the tcl payload shown above? Y|N" + set answer [gets stdin] + fconfigure stdin -blocking [dict get $stdin_state -blocking] + 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 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 + close $fdtarget + puts stdout "Wrote script file at $output_file" + puts stdout "-done-" + return $output_file + } + #require current dir when calling to be the projectdir, or proc newmodule {module args} { set year [clock format [clock seconds] -format %Y] @@ -187,16 +423,12 @@ namespace eval punk::mix::cli { set testdir [pwd] - if {[file tail $testdir] in [list "bin" "lib" "modules" "src"]} { - set testdir [file dirname $testdir] - if {[file tail $testdir] eq "src"} { - set testdir [file dirname $testdir] + if {![string length [set projectdir [punk::repo::find_project_root $testdir]]]} { + if {![string length [set projectdir [punk::repo::find_candidate_root $testdir]]]} { + error "newmodule unable to create module in projectdir:$projectdir - directory doesn't appear to meet basic standards (/src, src/modules, src/lib & /modules must exist, must not be a system path such as /usr/bin or c:/windows)" } } - set projectdir $testdir - if {![lib::is_project_dir $projectdir]} { - error "newmodule unable to create module in projectdir:$projectdir - directory doesn't appear to be an existing project" - } + if {$opt_project == "\uFFFF"} { set projectname [file tail $projectdir] } else { @@ -213,6 +445,7 @@ namespace eval punk::mix::cli { if {$opt_type ni [lib::module_types]} { error "mix newmodule - error - unknown -type '$opt_type' known-types: [lib::module_types]" } + set subpath [lib::module_subpath $modulename] ;#commonly empty string for simple modulename e.g "mymodule" but x::mymodule has subpath 'x' and x::y::mymodule has subpath 'x/y' if {![string length $subpath]} { set modulefolder $projectdir/src/modules @@ -247,19 +480,66 @@ namespace eval punk::mix::cli { proc make {args} { set startdir [pwd] - if {[lib::is_project_dir $startdir]} { - cd $startdir/src - set sourcefolder $startdir/src + set project_base "" ;#empty for unknown + if {[punk::repo::is_git $startdir]} { + set project_base [punk::repo::find_git] + set sourcefolder $project_base/src + } elseif {[punk::repo::is_fossil $startdir]} { + set project_base [punk::repo::find_fossil] + set sourcefolder $project_base/src } else { - set sourcefolder $startdir + if {[punk::repo::is_candidate $startdir]} { + set project_base [punk::repo::find_candidate] + set sourcefolder $project_base/src + puts stderr "WARNING - project not under git or fossil control" + puts stderr "Using base folder $project_base" + } else { + set sourcefolder $startdir + } } + + #review - why can't we be anywhere in the project? if {([file tail $sourcefolder] ne "src") || (![file exists $sourcefolder/make.tcl])} { puts stderr "mix make must be run from src folder containing make.tcl - unable to proceed (cwd: [pwd])" + if {[string length $project_base]} { + if {[file exists $project_base/src] && [string tolower [pwd]] ne [string tolower $projectbase/src]} { + puts stderr "Try cd to $project_base/src" + } + } return false } + + if {![string length $project_base]} { + puts stderr "WARNING no git or fossil repository detected." + puts stderr "Using base folder $startdir" + set project_base $startdir + } + + set lc_this_exe [string tolower [info nameofexecutable]] + set lc_proj_bin [string tolower $project_base/bin] + set lc_build_bin [string tolower $project_base/src/_build] + + + set is_own_exe 0 + if {[string match "${lc_proj_bin}*" $lc_this_exe] || [string match "${lc_build_bin}" $lc_this_exe]} { + set is_own_exe 1 + puts stderr "WARNING - running make using executable that may be created by the project being built" + puts stdout "Do you want to proceed using this executable? (build will probably stop when it is unable to update the executable) Y|N" + set stdin_state [fconfigure stdin] + fconfigure stdin -blocking 1 + set answer [gets stdin] + fconfigure stdin -blocking [dict get $stdin_state -blocking] + if {$answer ne "Y"} { + puts stderr "mix new aborting due to user response '$answer' (required Y to proceed) use -confirm 0 to avoid prompts." + return + } + } + cd $sourcefolder #use run so that stdout visible as it goes - set exitinfo [run [info nameofexecutable] $sourcefolder/make.tcl] + set exitinfo [run [info nameofexecutable] $sourcefolder/make.tcl project] set exitcode [dict get $exitinfo exitcode] + + cd $startdir if {$exitcode != 0} { puts stderr "FAILED with exitcode $exitcode" return false @@ -270,128 +550,10 @@ namespace eval punk::mix::cli { } proc Kettle {args} { - tailcall kettle_call lib {*}$args + tailcall lib::kettle_call lib {*}$args } proc KettleShell {args} { - tailcall kettle_call shell {*}$args - } - - proc kettle_call {calltype args} { - if {$calltype ni [list lib shell]} { - error "pmix kettle_call 1st argument must be one of: 'lib' for direct use of kettle module or 'shell' to call as separate process" - } - if {$calltype eq "shell"} { - set kettleappfile [file dirname [info nameofexecutable]]/kettle - set kettlebatfile [file dirname [info nameofexecutable]]/kettle.bat - - if {(![file exists $kettleappfile]) && (![file exists $kettlebatfile])} { - error "pmix kettle_call unable to find installed kettle application file '$kettleappfile' (or '$kettlebatfile' if on windows)" - } - if {[file exists $kettleappfile]} { - set kettlescript $kettleappfile - } - if {$::tcl_platform(platform) eq "windows"} { - if {[file exists $kettlebatfile]} { - set kettlescript $kettlebatfile - } - } - } - set startdir [pwd] - if {![file exists $startdir/build.tcl]} { - error "pmix kettle must be run from a folder containing build.tcl (cwd: [pwd])" - } - if {[catch {package present kettle}]} { - puts stdout "Loading kettle package - may be delay on first load ..." - package require kettle - } - set first [lindex $args 0] - if {[string match @* $first]} { - error "pmix kettle doesn't support special operations - try calling tclsh kettle directly" - } - if {$first eq "-f"} { - set args [lassign $args __ path] - } else { - set path $startdir/build.tcl - } - set opts [list] - - if {[lindex $args 0] eq "-trace"} { - set args [lrange $args 1 end] - lappend opts --verbose on - } - set goals [list] - - if {$calltype eq "lib"} { - file mkdir ~/.kettle - set dotfile ~/.kettle/config - if {[file exists $dotfile] && - [file isfile $dotfile] && - [file readable $dotfile]} { - ::kettle io trace {Loading dotfile $dotfile ...} - set args [list {*}[::kettle path cat $dotfile] {*}$args] - } - } - - #hardcoded kettle option names (::kettle option names) - retrieved using kettle::option names - #This is done so we don't have to load kettle lib for shell call (both loading as module and running shell are annoyingly SLOW) - #REVIEW - needs to be updated to keep in sync with kettle. - set knownopts [list\ - --exec-prefix --bin-dir --lib-dir --prefix --man-dir --html-dir --markdown-dir --include-dir \ - --ignore-glob --dry --verbose --machine --color --state --config --with-shell --log \ - --log-append --log-mode --with-dia --constraints --file --limitconstraints --tmatch --notfile --single --valgrind --tskip --repeats \ - --iters --collate --match --rmatch --with-doc-destination --with-git --target --test-include \ - ] - - while {[llength $args]} { - set o [lindex $args 0] - switch -glob -- $o { - --* { - #instead of using: kettle option known - if {$o ni $knownopts} { - error "Unable to process unknown option $o." {} [list KETTLE (pmix)] - } - lappend opts $o [lindex $args 1] - #::kettle::option set $o [lindex $args 1] - set args [lrange $args 2 end] - } - default { - lappend goals $o - set args [lrange $args 1 end] - } - } - } - - if {![llength $goals]} { - lappend goals help - } - if {"--prefix" ni [dict keys $opts]} { - dict set opts --prefix [file dirname $startdir] - } - if {$calltype eq "lib"} { - ::kettle status clear - ::kettle::option::set @kettle $startdir - foreach {o v} $opts { - ::kettle option set $o $v - } - ::kettle option set @srcscript $path - ::kettle option set @srcdir [file dirname $path] - ::kettle option set @goals $goals - ::source $path - puts stderr "recipes: [::kettle recipe names]" - ::kettle recipe run {*}[::kettle option get @goals] - - set state [::kettle option get --state] - if {$state ne {}} { - puts stderr "saving kettle state: $state" - ::kettle status save $state - } - - } else { - #shell - puts stdout "Running external kettle process with args: $opts $goals" - run -n tclsh $kettlescript -f $path {*}$opts {*}$goals - } - + tailcall lib::kettle_call shell {*}$args } #proc libexample {} { @@ -413,6 +575,29 @@ namespace eval punk::mix::cli { set nsq [namespace qualifiers $modulename] return [string map [list :: /] $nsq] } + #find src/something folders which are not certain known folders with other purposes, (such as: .vfs folders or vendor folders etc) and contain .tm file(s) + proc find_source_module_paths {{path {}}} { + if {![string length [set candidate [punk::repo::find_candidate $path]]]} { + error "find_source_module_paths cannot determine a suitable project root at or above path '$path' - path supplied should be within a project" + } + #we can return module paths even if the project isn't yet under revision control + set src_subs [glob -nocomplain -dir $candidate/src -type d -tail *] + set antipatterns [list *.vfs vendor* lib _build doc embedded runtime] + set tm_folders [list] + foreach sub $src_subs { + foreach anti $antipatterns { + if {[string match $anti $sub]} { + continue + } + } + set testfolder $candidate/src/$sub + set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm] + if {[llength $tmfiles]} { + lappend tm_folders $testfolder + } + } + return $tm_folders + } proc validate_modulename {modulename {name_description modulename}} { validate_name_not_empty_or_spaced $modulename $name_description set testname [string map [list :: ""] $modulename] @@ -429,7 +614,7 @@ namespace eval punk::mix::cli { } proc validate_projectname {projectname {name_description projectname}} { validate_name_not_empty_or_spaced $projectname $name_description - set reserved_words [list etc lib bin modules src doc man html tests] + set reserved_words [list etc lib bin modules src doc vendorlib vendormodules embedded runtime _aside _build] if {$projectname in $reserved_words } { error "$name_description '$projectname' cannot be one of reserved_words: $reserved_words" } @@ -447,30 +632,61 @@ namespace eval punk::mix::cli { } return $name } - proc is_project_dir {path} { - #review - find a reliable simple mechanism. Noting we have projects based on different templates. - #Should there be a specific required 'project' file of some sort? + proc get_build_cksums_stored {path} { + set buildfolder [get_build_folder $path] - #exclude some known places we wouldn't want to put a project - set normpath [file normalize $path] - set unwise_paths [list "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"] - if {[string tolower $normpath] in $unwise_paths} { - return 0 + set vfscontainer [file dirname $buildfolder] + set vfslist [glob -nocomplain -dir $vfscontainer -type d -tail *.vfs] + set dict_cksums [dict create] + foreach vfs $vfslist { + set vname [file rootname $vfs] + set dict_vfs [list $vname.vfs "" $vname.exe "" buildruntime.exe ""] + set ckfile $buildfolder/$vname.cksums + if {[file exists $ckfile]} { + set data [punk::repo::fcat -translation binary $ckfile] + foreach ln [split $data \n] { + if {[string trim $ln] eq ""} {continue} + lassign $ln path cksum + dict set dict_vfs $path $cksum + } + } + dict set dict_cksums $vname $dict_vfs } - - if {![file exists $path/src]} { - return 0 + return $dict_cksums + } + proc get_build_folder {path} { + if {[string length [set testbase [punk::repo::find_fossil $path]]]} { + set base $testbase + } elseif {[string length [set testbase [punk::repo::find_git $path]]]} { + set base $testbase + } elseif {[string length [set testbase [punk::repo::find_candidate $path]]]} { + set base $testbase + } else { + error "get_build_cksums_stored unable to determine project base for path '$path'" } - - #test for file/folder items indicating fossil or git workdir base - #todo - review .fossil check.. workdir checkout dir can be unrelate to *.fossil location - need to check fossil config? - set fossil_items [glob -nocomplain -dir $path *.fossil _FOSSIL_] - set git_items [glob -nocomplain -dir $path .gitignore .git] - if {([llength $fossil_items] < 1) && ([llength $git_items] < 1) } { - return 0 + if {![file exists $base/src] || ![file writable $base/src]} { + error "get_build_cksums_stored unable to access $base/src" } - return 1 + file mkdir $base/src/_build + return $base/src/_build } + proc get_build_cksums {path} { + set buildfolder [get_build_folder $path] + set vfscontainer [file dirname $buildfolder] + set vfslist [glob -nocomplain -dir $vfscontainer -type d -tail *.vfs] + set buildruntime $buildfolder/buildruntime.exe + set ckinfo_buildruntime [punk::repo::cksum_path $buildruntime] + set dict_cksums [dict create] + foreach vfs $vfslist { + set vname [file rootname $vfs] + set ckinfo_vfs [punk::repo::cksum_path $vfscontainer/$vname.vfs] + set ckinfo_exe [punk::repo::cksum_path $buildfolder/$vname.exe] + set dict_vfs [list $vname.vfs [dict get $ckinfo_vfs cksum] $vname.exe [dict get $ckinfo_exe cksum] buildruntime.exe [dict get $ckinfo_buildruntime cksum]] + dict set dict_cksums $vname $dict_vfs + } + return $dict_cksums + } + proc mix_templates_dir {} { set provide_statement [package ifneeded punk::mix [package require punk::mix]] set tmdir [file dirname [lindex $provide_statement end]] @@ -519,6 +735,243 @@ namespace eval punk::mix::cli { return 1 } + + #todo - package up + #todo - use winpath module's illegal name fix on windows + proc du_lit value { + if {![info exists ::punk::du_literal($value)]} { + set ::punk::du_literal($value) $value + } + return $::punk::du_literal($value) + } + proc _du_new_eachdir {dirtails depth parentfolderidx} { + set newlist {} + upvar folders folders + set parentpath [lindex $folders $parentfolderidx] + set newindex [llength $folders] + foreach dt $dirtails { + lappend folders $parentpath/$dt + lappend newlist [::list $depth $parentfolderidx $newindex $dt [expr {0}]] + incr newindex + } + return $newlist + } + proc du_listing {folderpath} { + #note platform differences between what is considered hidden make this tricky. + # on windows 'glob .*' will not return some hidden dot items but will return . .. and glob -types hidden .* will not return some dotted items + # glob -types hidden * on windows will not necessarily return all dot files/folders + # unix-like platforms seem to consider all dot files as hidden so processing is more straightforward + # we need to process * and .* in the same glob calls and remove duplicates + # if we do * and .* in separate iterations of this loop we lose the ability to filter duplicates easily + + #Note - glob operations seem to be faster than looped tests like 'file isdirectory' & 'file readlink' + #set parent [lindex $folders $folderidx] + set hdirs [glob -nocomplain -dir $folderpath -types {hidden d} -tail * .*] + #set hdirs {} + set dirs [glob -nocomplain -dir $folderpath -types d -tail * .*] + + #set hlinks [glob -nocomplain -dir [lindex $folders $folderidx] -types {hidden l} -tail * .*] + set hlinks {} + set links [glob -nocomplain -dir $folderpath -types l -tail * .*] ;#links may have dupes - we don't care. struct::set difference will remove + #set links [lsort -unique [concat $hlinks $links[unset links]]] + + set hfiles [glob -nocomplain -dir $folderpath -types {hidden f} -tail * .*] + #set hfiles {} + set files [glob -nocomplain -dir $folderpath -types f -tail * .*] + + #note struct::set difference produces unordered result + #struct::set difference removes duplicates + #remove links and . .. from directories, remove links from files + set dirs [struct::set difference [concat $hdirs $dirs[unset dirs]] [concat $links {. ..}]] + set files [struct::set difference [concat $hfiles $files[unset files]] $links] + return [list $dirs $files] + } + proc du { args } { + package require struct::set + switch -exact [llength $args] { + 0 { + set dir . + set switch -k + } + 1 { + set dir $args + set switch -k + } + 2 { + set switch [lindex $args 0] + set dir [lindex $args 1] + } + default { + set msg "only one switch and one dir " + append msg "currently supported" + return -code error $msg + } + } + + set switch [string tolower $switch] + + set -b 1 + set -k 1024 + set -m [expr 1024*1024] + + set result [list] + + set dir_depths_remaining [list] + + #if {[file normalize [file dirname $dir]] eq [file normalize $dir]} { + # error "du at root of filesystem not yet implemented.. sorry" + # + #} + + set zero [expr {0}] + + # ## ### ### ### ### + # containerid and itemid + set folders [list] ;#we lookup string by index + lappend folders [file dirname $dir] + lappend folders $dir ;#itemindex 1 + # ## ### ### ### ### + if {![file isdirectory $dir]} { + lappend dir_depths_remaining [list $zero $zero [expr {1}] [du_lit [file tail $dir]] [file size $dir]] + #set ary($dir,bytes) [file size $dir] + set leveldircount 0 + } else { + lappend dir_depths_remaining [list $zero $zero [expr {1}] [du_lit [file tail $dir]] $zero] + set leveldircount 1 + } + set level [expr {0}] + set nextlevel [expr {1}] + #dir_depths list structure + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + #0 1 2 3 4 5 + #i_depth i_containerid i_itemid i_item i_size i_index + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + set i_depth [expr {0}] + set i_containerid [expr {1}] + set i_itemid [expr {2}] + set i_item [expr {3}] + set i_size [expr {4}] + set i_index [expr {5}] + + + set listlength [llength $dir_depths_remaining] + set diridx 0 + #this is a breadth-first algorithm + while {$leveldircount > 0} { + #set leveldirs [list] + for {set i $diridx} {$i < $listlength} {incr i} { + #lassign [lindex $dir_depths_remaining $i] _d containeridx folderidx itm bytecount + set folderidx [lindex $dir_depths_remaining $i $i_itemid] + set folderpath [lindex $folders $folderidx] + + lassign [du_listing $folderpath] dirs files + + #lappend dir_depths_remaining {*}[lmap d $dirs {::list $nextdepth [du_lit $cont/$itm] $d $zero}] + #folderidx is parent index for new dirs + lappend dir_depths_remaining {*}[_du_new_eachdir $dirs $nextlevel $folderidx] + + #we don't need to sort files (unless we add an option such as -a to du (?)) + set bytecount [expr {0}] + foreach filename $files { + incr bytecount [file size "$folderpath/$filename"] + } + #we can safely assume initial count was zero + lset dir_depths_remaining $i $i_size $bytecount + #incr diridx + } + #puts stdout "level: $level dirs: $leveldirs" + incr level ;#zero based + set nextlevel [expr {$level + 1}] + set leveldircount [expr {[llength $dir_depths_remaining] - $listlength }]; #current - previous - while loop terminates when zero + #puts "diridx: $diridx i: $i rem: [llength $dir_depths_remaining] listlenth:$listlength levldircount: $leveldircount" + set diridx $i + set listlength [llength $dir_depths_remaining] + } + #puts stdout ">>> loop done" + #flush stdout + #puts stdout $dir_depths_remaining + set dirs_as_encountered $dir_depths_remaining ;#index is in sync with 'folders' list + set dir_depths_longfirst $dirs_as_encountered + + #store the index before sorting + for {set i 0} {$i < [llength $dir_depths_remaining]} {incr i} { + lset dir_depths_longfirst $i $i_index $i + } + set dir_depths_longfirst [lsort -integer -index 0 -decreasing $dir_depths_longfirst[set dir_depths_longfirst {}]] + + #store main index in the reducing list + set dir_depths_remaining $dir_depths_longfirst + for {set i 0} {$i < [llength $dir_depths_remaining]} {incr i} { + #stored index at position 3 + lset dir_depths_remaining $i $i_index $i + } + + #index 3 + #dir_depths_remaining -> dir_depths_longfirst -> dirs_as_encountered + + #puts stdout "initial dir_depths_remaining: $dir_depths_remaining" + + #summing performance is not terrible - the real time is for large trees in the main loop above + if {[llength $dir_depths_longfirst] > 1} { + set i 0 + foreach dd $dir_depths_longfirst { + lassign $dd d parentidx folderidx item bytecount + #set nm $cont/$item + set nm [lindex $folders $folderidx] + set dnext [expr {$d +1}] + set nextdepthposns [lsearch -all -integer -index 0 $dir_depths_remaining $dnext] + set nextdepthposns [lsort -integer -decreasing $nextdepthposns[set nextdepthposns {}]];#remove later elements first + foreach posn $nextdepthposns { + set id [lindex $dir_depths_remaining $posn $i_itemid] + set ndirname [lindex $folders $id] + #set ndirname $cont/$item + #set item [lindex $dir_depths_remaining $posn $i_item] + #set ndirname [lindex $ndir 1] + if {[string match $nm/* $ndirname]} { + #puts stdout "dir $nm adding subdir size $ndirname" + #puts stdout "incr $nm from $ary($nm,bytes) plus $ary($ndirname,bytes)" + incr bytecount [lindex $dir_depths_remaining $posn $i_size] + set dir_depths_remaining [lreplace $dir_depths_remaining[set dir_depths_remaining {}] $posn $posn] + } + } + lset dir_depths_longfirst $i $i_size $bytecount + set p [lsearch -index $i_index -integer $dir_depths_remaining $i] + lset dir_depths_remaining $p $i_size $bytecount + #set ary($nm,bytes) $bytecount + incr i + } + } + #set dir_depths_longfirst [lsort -index 1 -decreasing $dir_depths_longfirst] + # + + #copy across the bytecounts + for {set i 0} {$i < [llength $dir_depths_longfirst]} {incr i} { + set posn [lindex $dir_depths_longfirst $i $i_index] + set bytes [lindex $dir_depths_longfirst $i $i_size] + lset dirs_as_encountered $posn $i_size $bytes + } + foreach dirinfo [lreverse $dirs_as_encountered] { + set id [lindex $dirinfo $i_itemid] + set path [lindex $folders $id] + #set path $cont/$item + set item [lindex $dirinfo $i_item] + set bytes [lindex $dirinfo $i_size] + set size [expr {$bytes / [set $switch]}] + lappend retval [list $size $path] + } + # copyright 2002 by The LIGO Laboratory + return $retval + } + package require natsort + #interp alias {} du {} .=args>* punk::mix::cli::lib::du |> .=>1 natsort::sort -cols 1 |> list_as_lines * punk::mix::cli::lib::du |> .=>1 natsort::sort -cols 1 -outputformat words |> list_as_lines * punk::mix::cli::lib::du |> .=>1 natsort::sort -cols 1 -outputformat csv -outputformatoptions {\r\t\t\t} |> list_as_lines build_modules_from_source_to_base /x/src/modules2 /x/modules2 -subdirlist {skunkworks lib}" + exit 2 } - + set srcdirname [file tail $srcdir] + + set build [file dirname $srcdir]/_build/$srcdirname ;#relative to *original* srcdir - not current_source_dir if {[llength $subdirlist] == 0} { set target_module_dir $basedir + set current_source_dir $srcdir } else { set target_module_dir $basedir/[file join {*}$subdirlist] + set current_source_dir $srcdir/[file join {*}$subdirlist] } + if {![file exists $target_module_dir]} { + error "build_modules_from_source_to_base from current source dir: '$current_source_dir'. Basedir:'$current_module_dir' doesn't exist or is empty" + } + if {![file exists $current_source_dir]} { + error "build_modules_from_source_to_base from current source dir:'$current_source_dir' doesn't exist or is empty" + } + + set src_modules [glob -nocomplain -dir $current_source_dir -type f -tail $fileglob] + + foreach m $src_modules { set fileparts [split [file rootname $m] -] set tmfile_versionsegment [lindex $fileparts end] if {$tmfile_versionsegment eq $magicversion} { #rebuild the .tm from the #tarjar set basename [join [lrange $fileparts 0 end-1] -] - set versionfile $srcdir/$basename-buildversion.txt + set versionfile $current_source_dir/$basename-buildversion.txt if {![file exists $versionfile]} { - puts stderr "ERROR: Missing buildversion text file: $versionfile" - } - set fd [open $versionfile r]; set data [read $fd]; close $fd - set ln0 [lindex [split $data \n] 0] - set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] - if {![is_valid_tm_version $ln0]} { - puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" - exit 3 + puts stderr "WARNING: Missing buildversion text file: $versionfile" + puts stderr "Using version 0.1 - create $versionfile containing the desired version number as the top line to avoid this warning" + set module_build_version "0.1" + } else { + set fd [open $versionfile r]; set data [read $fd]; close $fd + set ln0 [lindex [split $data \n] 0] + set ln0 [string trim $ln0]; set ln0 [string trim $ln0 \r] + if {![is_valid_tm_version $ln0]} { + puts stderr "ERROR: build version '$ln0' specified in $versionfile is not suitable. Please ensure a proper version number is at first line of file" + exit 3 + } + set module_build_version $ln0 } - set module_build_version $ln0 - if {[file exists $srcdir/#tarjar-$basename-$magicversion]} { - if {[file exists $srcdir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { + + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion]} { + file mkdir $buildfolder + + if {[file exists $current_source_dir/#tarjar-$basename-$magicversion/DESCRIPTION.txt]} { } else { } - + #REVIEW - should be in same structure/depth as $target_module_dir in _build? set tmfile $basedir/_build/$basename-$module_build_version.tm file mkdir $basedir/_build file delete -force $basedir/_build/#tarjar-$basename-$module_build_version file delete -force $tmfile - file copy -force $srcdir/#tarjar-$basename-$magicversion $basedir/_build/#tarjar-$basename-$module_build_version + file copy -force $current_source_dir/#tarjar-$basename-$magicversion $basedir/_build/#tarjar-$basename-$module_build_version # #bsdtar doesn't seem to work.. or I haven't worked out the right options? #exec tar -cvf $basedir/_build/$basename-$module_build_version.tm $basedir/_build/#tarjar-$basename-$module_build_version @@ -682,15 +1220,19 @@ namespace eval punk::mix::cli { puts stdout "ERROR: Failed to build tarjar file $tmfile" exit 4 } + #copy the file? + #set target $target_module_dir/$basename-$module_build_version.tm + #file copy -force $tmfile $target + lappend module_list $tmfile } else { #assume that either the .tm is not a tarjar - or the tarjar dir is capped (trailing #) and the .tm has been manually tarred. - if {[file exists $srcdir/#tarjar-$basename-${magicversion}#]} { + if {[file exists $current_source_dir/#tarjar-$basename-${magicversion}#]} { puts stderr "Warning: found 'capped' folder #tarjar-$basename-${magicversion}# - No attempt being made to update version in description.txt" } set target $target_module_dir/$basename-$module_build_version.tm - puts stdout "copying module $srcdir/$m to $target as version: $module_build_version ([file tail $target])" - set fd [open $srcdir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd + puts stdout "copying module $current_source_dir/$m to $target as version: $module_build_version ([file tail $target])" + set fd [open $current_source_dir/$m r]; fconfigure $fd -translation binary; set data [read $fd]; close $fd set data [string map [list $magicversion $module_build_version] $data] set fdout [open $target w] fconfigure $fdout -translation binary @@ -706,26 +1248,147 @@ namespace eval punk::mix::cli { if {![is_valid_tm_version $tmfile_versionsegment]} { #last segment doesn't look even slightly versiony - fail. - puts stderr "ERROR: Unable to confirm file $srcdir/$m is a reasonably versioned .tm module - ABORTING." + puts stderr "ERROR: Unable to confirm file $current_source_dir/$m is a reasonably versioned .tm module - ABORTING." exit 1 } - puts stderr "copying already versioned module $srcdir/$m to $target_module_dir" - file copy -force $srcdir/$m $target_module_dir - lappend module_list $srcdir/$m + puts stderr "copying already versioned module $current_source_dir/$m to $target_module_dir" + file copy -force $current_source_dir/$m $target_module_dir + lappend module_list $current_source_dir/$m } - set subdirs [glob -nocomplain -dir $srcdir -type d -tail *] + set subdirs [glob -nocomplain -dir $current_source_dir -type d -tail *] #puts stderr "subdirs: $subdirs" foreach d $subdirs { - if {[string match "#*" $d] || ($d eq "_aside") || ($d eq ".git")} { - continue + foreach dg $antidir { + if {[string match $dg $d]} { + continue + } } if {![file exists $target_module_dir/$d]} { file mkdir $target_module_dir/$d } - lappend module_list {*}[build_modules_from_source_to_base $srcdir/$d $basedir -subdirlist [list {*}$subdirlist $d] -glob $fileglob] + lappend module_list {*}[build_modules_from_source_to_base $srcdir $basedir -subdirlist [list {*}$subdirlist $d] -glob $fileglob] } return $module_list } + + proc kettle_call {calltype args} { + if {$calltype ni [list lib shell]} { + error "pmix kettle_call 1st argument must be one of: 'lib' for direct use of kettle module or 'shell' to call as separate process" + } + if {$calltype eq "shell"} { + set kettleappfile [file dirname [info nameofexecutable]]/kettle + set kettlebatfile [file dirname [info nameofexecutable]]/kettle.bat + + if {(![file exists $kettleappfile]) && (![file exists $kettlebatfile])} { + error "pmix kettle_call unable to find installed kettle application file '$kettleappfile' (or '$kettlebatfile' if on windows)" + } + if {[file exists $kettleappfile]} { + set kettlescript $kettleappfile + } + if {$::tcl_platform(platform) eq "windows"} { + if {[file exists $kettlebatfile]} { + set kettlescript $kettlebatfile + } + } + } + set startdir [pwd] + if {![file exists $startdir/build.tcl]} { + error "pmix kettle must be run from a folder containing build.tcl (cwd: [pwd])" + } + if {[catch {package present kettle}]} { + puts stdout "Loading kettle package - may be delay on first load ..." + package require kettle + } + set first [lindex $args 0] + if {[string match @* $first]} { + error "pmix kettle doesn't support special operations - try calling tclsh kettle directly" + } + if {$first eq "-f"} { + set args [lassign $args __ path] + } else { + set path $startdir/build.tcl + } + set opts [list] + + if {[lindex $args 0] eq "-trace"} { + set args [lrange $args 1 end] + lappend opts --verbose on + } + set goals [list] + + if {$calltype eq "lib"} { + file mkdir ~/.kettle + set dotfile ~/.kettle/config + if {[file exists $dotfile] && + [file isfile $dotfile] && + [file readable $dotfile]} { + ::kettle io trace {Loading dotfile $dotfile ...} + set args [list {*}[::kettle path cat $dotfile] {*}$args] + } + } + + #hardcoded kettle option names (::kettle option names) - retrieved using kettle::option names + #This is done so we don't have to load kettle lib for shell call (both loading as module and running shell are annoyingly SLOW) + #REVIEW - needs to be updated to keep in sync with kettle. + set knownopts [list\ + --exec-prefix --bin-dir --lib-dir --prefix --man-dir --html-dir --markdown-dir --include-dir \ + --ignore-glob --dry --verbose --machine --color --state --config --with-shell --log \ + --log-append --log-mode --with-dia --constraints --file --limitconstraints --tmatch --notfile --single --valgrind --tskip --repeats \ + --iters --collate --match --rmatch --with-doc-destination --with-git --target --test-include \ + ] + + while {[llength $args]} { + set o [lindex $args 0] + switch -glob -- $o { + --* { + #instead of using: kettle option known + if {$o ni $knownopts} { + error "Unable to process unknown option $o." {} [list KETTLE (pmix)] + } + lappend opts $o [lindex $args 1] + #::kettle::option set $o [lindex $args 1] + set args [lrange $args 2 end] + } + default { + lappend goals $o + set args [lrange $args 1 end] + } + } + } + + if {![llength $goals]} { + lappend goals help + } + if {"--prefix" ni [dict keys $opts]} { + dict set opts --prefix [file dirname $startdir] + } + if {$calltype eq "lib"} { + ::kettle status clear + ::kettle::option::set @kettle $startdir + foreach {o v} $opts { + ::kettle option set $o $v + } + ::kettle option set @srcscript $path + ::kettle option set @srcdir [file dirname $path] + ::kettle option set @goals $goals + ::source $path + puts stderr "recipes: [::kettle recipe names]" + ::kettle recipe run {*}[::kettle option get @goals] + + set state [::kettle option get --state] + if {$state ne {}} { + puts stderr "saving kettle state: $state" + ::kettle status save $state + } + + } else { + #shell + puts stdout "Running external kettle process with args: $opts $goals" + run -n tclsh $kettlescript -f $path {*}$opts {*}$goals + } + + } + } } diff --git a/src/modules/punk/mix/base-0.1.tm b/src/modules/punk/mix/base-0.1.tm index b99fa92f..ad1f4c5e 100644 --- a/src/modules/punk/mix/base-0.1.tm +++ b/src/modules/punk/mix/base-0.1.tm @@ -207,10 +207,9 @@ namespace eval punk::mix::base { } return $helpstr } - proc dostuff {args} { - extension@@opts/@?@-extension,args@@args= [_split_args $args] - - puts stdout "base doingstuff-with-args:'$args'-in-namespace:'[namespace current]'" - } + #proc dostuff {args} { + # extension@@opts/@?@-extension,args@@args= [_split_args $args] + # puts stdout "base doingstuff-with-args:'$args'-in-namespace:'[namespace current]'" + #} } diff --git a/src/modules/punk/mix/templates/layouts/project/.fossil-settings/empty-dirs b/src/modules/punk/mix/templates/layouts/project/.fossil-settings/empty-dirs index ee61a81e..f8f80ad7 100644 --- a/src/modules/punk/mix/templates/layouts/project/.fossil-settings/empty-dirs +++ b/src/modules/punk/mix/templates/layouts/project/.fossil-settings/empty-dirs @@ -1,3 +1,7 @@ src -src/deps +src/vendorlib +src/vendormodules src/modules +src/lib +lib +modules diff --git a/src/modules/punk/mix/templates/layouts/project/src/README.md b/src/modules/punk/mix/templates/layouts/project/src/README.md index a9557146..972fd1ab 100644 --- a/src/modules/punk/mix/templates/layouts/project/src/README.md +++ b/src/modules/punk/mix/templates/layouts/project/src/README.md @@ -6,7 +6,7 @@ Build Instructions + Use tclsh|punk make.tcl to build .tm modules and rebuild the executable (if applicable) -+ Then Use tclsh|punk build.tcl to run the 'kettle' system to build docs and/or standard tcl libraries with pkgIndex.tcl files, ++ Use tclsh|punk build.tcl to run the 'kettle' system to build docs and/or standard tcl libraries with pkgIndex.tcl files, or - use the `pmix KettleShell` command from within the punk shell to perform kettle operations. (The name 'build.tcl' is the standard name used by the [Kettle](https://chiselapp.com/user/andreas_kupries/repository/Kettle/home) system) diff --git a/src/modules/punk/mix/templates/layouts/project/src/embedded/README.md b/src/modules/punk/mix/templates/layouts/project/src/embedded/README.md new file mode 100644 index 00000000..4f99e416 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/embedded/README.md @@ -0,0 +1,5 @@ +Documents and help files (for the repository website) +These are html, markdown, manfiles etc which live within src/embedded and are intended to be checked into source control so they can form part of the online documentation available when browsing the repository. + +These files shouldn't be modified directly as they are built from the files in the src/doc folder +(Using the Kettle build system) diff --git a/src/modules/punk/mix/templates/layouts/project/src/lib/README.md b/src/modules/punk/mix/templates/layouts/project/src/lib/README.md new file mode 100644 index 00000000..143653c4 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/lib/README.md @@ -0,0 +1,7 @@ +Tcl Library Source files for the project. + +These are Tcl packages which use the pkgIndex system. + +The Kettle Build tool can be used to generate pkgIndex.tcl files and install these to appropriate locations. + + diff --git a/src/modules/punk/mix/templates/layouts/project/src/make.tcl b/src/modules/punk/mix/templates/layouts/project/src/make.tcl index dbf6d7f9..f053cd5f 100644 --- a/src/modules/punk/mix/templates/layouts/project/src/make.tcl +++ b/src/modules/punk/mix/templates/layouts/project/src/make.tcl @@ -40,13 +40,15 @@ if {![llength $vfs_folders]} { puts stdout " -done- " exit 0 } -file mkdir $sourcefolder/_build -if {[catch {exec sdx help} errM]} { - puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" - puts stderr "err: $errM" - exit 1 +set buildfolder [punk::mix::cli::lib::get_build_folder $sourcefolder] +if {$buildfolder ne "$sourcefolder/_build"} { + puts stderr "$sourcefolder/_build doesn't match the project buildfolder $buildfolder - check project filestructure" + puts stdout " -aborted- " + exit 2 } + + #find runtime - only supports one for now.. REVIEW set rtfolder $sourcefolder/runtime set runtimes [glob -nocomplain -dir $rtfolder -types {f x} -tail *] @@ -54,30 +56,44 @@ if {![llength $runtimes]} { puts stderr "No executable runtimes found in $rtfolder - unable to build any .vfs folders into executables." exit 2 } + +if {[catch {exec sdx help} errM]} { + puts stderr "FAILED to find usable sdx command - check that sdx executable is on path" + puts stderr "err: $errM" + exit 1 +} + + if {[llength $runtimes] > 1} { puts stderr "Found multiple runtimes in $rtfolder ($runtimes) - unable to proceed - currently limited to one." exit 3 } set runtimefile [lindex $runtimes 0] - +#sdx *may* be pointed to use the runtime we use to build the kit, or the user may manually use this runtime if they don't have tclsh +#sdx will complain if the same runtime is used for the shell as is used in the -runtime argument - so we make a copy (REVIEW) +if {![file exists $buildfolder/buildruntime.exe]} { + file copy $rtfolder/$runtimefile $buildfolder/buildruntime.exe +} +set startdir [pwd] puts stdout "Found [llength $vfs_folders] .vfs folders - building executable for each..." +cd [file dirname $buildfolder] foreach vfs $vfs_folders { set vfsname [file rootname $vfs] puts stdout " Processing vfs $sourcefolder/$vfs" puts stdout " ------------------------------------" - if {[file exists $sourcefolder/_build/$vfsname]} { - puts stderr "deleting existing $sourcefolder/_build/$vfsname" + if {[file exists $buildfolder/$vfsname]} { + puts stderr "deleting existing $buildfolder/$vfsname" file delete $sourcefolder/_build/$vfsname } - puts stdout "building $vfsname with sdx.." + puts stdout "building $vfsname with sdx.. vfsdir:$vfs cwd: [pwd]" if {[catch { - exec sdx wrap _build/$vfsname -runtime runtime/$runtimefile -verbose + exec sdx wrap $buildfolder/$vfsname -vfs $sourcefolder/$vfs -runtime $buildfolder/buildruntime.exe -verbose } result]} { - puts stderr "sdx wrap _build/$vfsname -runtime runtime/$runtimefile -verbose failed with msg: $result" + puts stderr "sdx wrap _build/$vfsname -vfs $sourcefolder/$vfs -runtime $buildfolder/buildruntime.exe -verbose failed with msg: $result" } else { puts stdout "ok - finished sdx" set separator [string repeat = 40] @@ -86,7 +102,7 @@ foreach vfs $vfs_folders { puts stdout $separator } - if {![file exists $sourcefolder/_build/$vfsname]} { + if {![file exists $buildfolder/$vfsname]} { puts stderr "|err> build didn't seem to produce output at $sourcefolder/_build/$vfsname" exit 2 } @@ -150,18 +166,18 @@ foreach vfs $vfs_folders { set targetexe $vfsname } - if {[file exists $sourcefolder/_build/$targetexe]} { - puts stderr "deleting existing $sourcefolder/_build/$targetexe" + if {[file exists $buildfolder/$targetexe]} { + puts stderr "deleting existing $buildfolder/$targetexe" if {[catch { file delete $sourcefolder/_build/$targetexe } msg]} { - puts stderr "Failed to delete $sourcefolder/_build/$targetexe" + puts stderr "Failed to delete $buildfolder/$targetexe" exit 4 } } if {$::tcl_platform(platform) eq "windows"} { - file rename $sourcefolder/_build/$vfsname $sourcefolder/_build/${vfsname}.exe + file rename $buildfolder/$vfsname $sourcefolder/_build/${vfsname}.exe } after 200 @@ -181,13 +197,13 @@ foreach vfs $vfs_folders { puts stdout "copying.." - puts stdout "$sourcefolder/_build/$targetexe" + puts stdout "$buildfolder/$targetexe" puts stdout "to:" puts stdout "$deployment_folder/$targetexe" after 500 - file copy $sourcefolder/_build/$targetexe $deployment_folder/$targetexe + file copy $buildfolder/$targetexe $deployment_folder/$targetexe } - +cd $startdir puts stdout "done" exit 0 diff --git a/src/modules/punk/mix/templates/layouts/project/src/modules/README.md b/src/modules/punk/mix/templates/layouts/project/src/modules/README.md new file mode 100644 index 00000000..1c037091 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/modules/README.md @@ -0,0 +1,11 @@ +Tcl Module Source files for the project. +Consider using the punkshell pmix facility to create and manage these. + +pmix::newmodule will create a basic .tm module template and assist in versioning. + +Tcl modules can be namespaced. +For example +> pmix::newmodule mymodule::utils +will create the new module under src/modules/mymodule/utils + + diff --git a/src/modules/punk/mix/templates/layouts/project/src/runtime/Readme.md b/src/modules/punk/mix/templates/layouts/project/src/runtime/Readme.md new file mode 100644 index 00000000..52e39297 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/runtime/Readme.md @@ -0,0 +1,3 @@ +Install a tclkit runtime here by running the appropriate fetchruntime script in ../src + +Alternatively the runtime can be downloaded from: https://www.gitea1.intx.com.au/jn/punkbin diff --git a/src/modules/punk/mix/templates/layouts/project/src/scriptapps/README.md b/src/modules/punk/mix/templates/layouts/project/src/scriptapps/README.md new file mode 100644 index 00000000..76e5030b --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/scriptapps/README.md @@ -0,0 +1,20 @@ +Create multishell scripts from your .tcl .sh and .ps1 scripts that are stored here. + +Use the pmix wrap functions to generate a multishell .cmd file from your scripts. +This .cmd is a 'polyglot' script - it should run when called from any of the target interpreters. + + +A multishell .cmd file is a cross-platform script that can easily be run on Windows and unix-like platforms. + +The .cmd extension is primarily a convenience so that it can be run easily by name on windows but it is ok to either leave it as that on other platforms, or rename it appropriately. + +On unix-like platforms it can be called with a bourne shell such as sh or bash. + +On windows, it can also be called with sh or bash if they are available - but the usual method would be to run it under cmd.exe initially just by opening a cmd prompt and running it. +This will run some windows batch script to automatically generate a corresponding .ps1 file and execution will switch to powershell 5 or powershell 7 (pwsh) if available. +Subsequently the command can be run directly from powershell. + +Whether called from Bourne shell, or cmd.exe or powershell - the usual payload would be your wrapped Tcl code - but it's also possible for powershell or sh/bash to be the primary payload script. +Any of these languages could easily be used to detect and launch other scripts/utilities that you may distribute with your app. + + diff --git a/src/modules/punk/mix/templates/layouts/project/src/vendorlib/README.md b/src/modules/punk/mix/templates/layouts/project/src/vendorlib/README.md new file mode 100644 index 00000000..eaf72fe4 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/vendorlib/README.md @@ -0,0 +1,8 @@ +Tcl library dependencies + +Any pkgIndex based libraries that are external to the project but which the project owners wish to distribute with the project and keep under source control. + +These should generally be kept to a minimum +- with dependency and version numbers being tracked instead; along with the provision of a mechanism for the project end-users to update. + + diff --git a/src/modules/punk/mix/templates/layouts/project/src/vendormodules/README.md b/src/modules/punk/mix/templates/layouts/project/src/vendormodules/README.md new file mode 100644 index 00000000..942fe325 --- /dev/null +++ b/src/modules/punk/mix/templates/layouts/project/src/vendormodules/README.md @@ -0,0 +1,7 @@ +Tcl module dependencies + +Any .tm files that are external to the project but which the project owners wish to distribute with the project and keep under source control. + +These should generally be kept to a minimum +- with dependency and version numbers being tracked instead; along with the provision of a mechanism for the project end-users to update. + diff --git a/src/modules/punk/mix/templates/utility/multishell.cmd b/src/modules/punk/mix/templates/utility/multishell.cmd new file mode 100644 index 00000000..9d903392 --- /dev/null +++ b/src/modules/punk/mix/templates/utility/multishell.cmd @@ -0,0 +1,264 @@ +set -- "$@" "a=[list shebangless punk MULTISHELL tclsh sh bash cmd pwsh powershell;proc Hide s {proc $s args {}}; Hide :;rename set s2;Hide set;s2 1 list]"; set -- : "$@"; $1 = @' +: heredoc1 - hide from powershell (close sqote for unix shells) ' \ +: << 'HEREDOC1B_HIDE_FROM_BASH_AND_SH' +: .bat/.cmd launch section, leading colon hides from cmd, trailing slash hides next line from tcl \ +: "[Hide @ECHO; Hide ); Hide (;Hide echo]#not necessary but can help avoid errs in testing" +: Continuation char at end of this line and rem with curly-braces used to exlude Tcl from the whole cmd block \ +@REM { +@REM DO NOT MODIFY FIRST LINE OF THIS SCRIPT. shebang #! line is not required and will reduce functionality. +@REM Even comment lines can be part of the functionality of this script - modify with care. +@REM Change the value of nextshell in the next line if desired, and code within payload sections as appropriate. +@SET "nextshell=pwsh" +@REM nextshell set to pwsh,sh,bash or tclsh +@REM @ECHO nextshell is %nextshell% +@SET "validshells=pwsh,sh,bash,tclsh" +@CALL SET keyRemoved=%%validshells:%nextshell%=%% +@REM Note that 'powershell' e.g v5 is just a fallback for when pwsh is not available +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@REM -- cmd/batch file section (ignored on unix) +@REM -- This section intended only to launch the next shell +@REM -- Avoid customising this if possible. cmd/batch script is probably the least expressive language. +@REM -- custom windows payloads should be in powershell,tclsh or sh/bash code sections +@REM ## ### ### ### ### ### ### ### ### ### ### ### ### ### +@SETLOCAL EnableExtensions EnableDelayedExpansion +@SET "winpath=%~dp0" +@SET "fname=%~nx0" +@REM @ECHO fname %fname% +@REM @ECHO winpath %winpath% +@IF %nextshell%==pwsh ( + CALL pwsh -nop -c set-executionpolicy -Scope CurrentUser RemoteSigned + COPY "%~dp0%~n0.cmd" "%~dp0%~n0.ps1" >NUL + REM test availability of preferred option of powershell7+ pwsh + CALL pwsh -nop -nol -c write-host "statusmessage: pwsh-found" >NUL + SET pwshtest_exitcode=!errorlevel! + REM ECHO pwshtest_exitcode !pwshtest_exitcode! + IF !pwshtest_exitcode!==0 CALL pwsh -nop -nol "%~dp0%~n0.ps1" %* & SET task_exitcode=!errorlevel! + REM fallback to powershell if pwsh failed + IF NOT !pwshtest_exitcode!==0 ( + REM CALL powershell -nop -nol -c write-host powershell-found + CALL powershell -nop -nol -file "%~dp0%~n0.ps1" %* + SET task_exitcode=!errorlevel! + ) +) ELSE ( + IF %nextshell%==bash ( + CALL :getWslPath %winpath% wslpath + REM ECHO wslfullpath "!wslpath!%fname%" + CALL %nextshell% "!wslpath!%fname%" %* & SET task_exitcode=!errorlevel! + ) ELSE ( + REM probably tclsh or sh + IF NOT "x%keyRemoved%"=="x%validshells%" ( + REM sh uses /c/ instead of /mnt/c - at least if using msys. Todo, review what is the norm on windows with and without msys2,cygwin,wsl + REM and what logic if any may be needed. For now sh with /c/xxx seems to work the same as sh with c:/xxx + CALL %nextshell% "%~dp0%fname%" %* & SET task_exitcode=!errorlevel! + ) ELSE ( + ECHO %fname% has invalid nextshell value %nextshell% valid options are %validshells% + SET task_exitcode=66 + GOTO :exit + ) + ) +) +@GOTO :endlib +:getWslPath +@SETLOCAL + @SET "_path=%~p1" + @SET "name=%~nx1" + @SET "drive=%~d1" + @SET "rtrn=%~2" + @SET "result=/mnt/%drive:~0,1%%_path:\=/%%name%" +@ENDLOCAL & ( + @if "%~2" neq "" ( + SET "%rtrn%=%result%" + ) ELSE ( + ECHO %result% + ) +) +@GOTO :eof +:endlib + +: \ +@REM @SET taskexit_code=!errorlevel! & goto :exit +@GOTO :exit +# } +# rem call %nextshell% "%~dp0%~n0.cmd" %* +# -*- tcl -*- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- tcl script section +# -- This is a punk multishell file +# -- Primary payload target is Tcl, with sh,bash,powershell as helpers +# -- but it may equally be used with any of these being the primary script. +# -- It is tuned to run when called as a batch file, a tcl script a sh/bash script or a pwsh/powershell script +# -- i.e it is a polyglot file. +# -- The specific layout including some lines that appear just as comments is quite sensitive to change. +# -- It can be called on unix or windows platforms with or without the interpreter being specified on the commandline. +# -- e.g ./filename.polypunk.cmd in sh or bash +# -- e.g tclsh filename.cmd +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +rename set ""; rename s2 set; set k {-- "$@" "a}; if {[info exists ::env($k)]} {unset ::env($k)} ;# tidyup +Hide :exit;Hide {<#};Hide '@ +namespace eval ::punk::multishell { + set last_script_root [file dirname [file normalize ${argv0}/__]] + set last_script [file dirname [file normalize [info script]/__]] + if {[info exists argv0] && + $last_script eq $last_script_root + } { + set ::punk::multishell::is_main($last_script) 1 ;#run as executable/script - likely desirable to launch application and return an exitcode + } else { + set ::punk::multishell::is_main($last_script) 0 ;#sourced - likely to be being used as a library - no launch, no exit. Can use return. + } + if {"::punk::multishell::is_main" ni [info commands ::punk::multishell::is_main]} { + proc ::punk::multishell::is_main {{script_name {}}} { + if {$script_name eq ""} { + set script_name [file dirname [file normalize [info script]/--]] + } + return [set ::punk::multishell::is_main($script_name)] + } + } +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" +#puts "argv0 : $::argv0" +# -- --- --- --- --- --- --- --- --- --- --- --- + + +# +# + + + +# -- --- --- --- --- --- --- --- --- --- --- --- +# -- Best practice is to always return or exit above, or just by leaving the below defaults in place. +# -- If the multishell script is modified to have Tcl below the Tcl Payload section, +# -- then Tcl bracket balancing needs to be carefully managed in the shell and powershell sections below. +# -- Only the # in front of the two relevant if statements below needs to be removed to enable Tcl below +# -- but the sh/bash 'then' and 'fi' would also need to be uncommented. +# -- This facility left in place for experiments on whether configuration payloads etc can be appended +# -- to tail of file - possibly binary with ctrl-z char - but utility is dependent on which other interpreters/shells +# -- can be made to ignore/cope with such data. +if {[::punk::multishell::is_main]} { + exit 0 +} else { + return +} +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +# end hide from unix shells \ +HEREDOC1B_HIDE_FROM_BASH_AND_SH +# sh/bash \ +shift && set -- "${@:1:$#-1}" +#------------------------------------------------------ +# -- This if block only needed if Tcl didn't exit or return above. +if false==false # else { + then + : +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- sh/bash script section +# -- leave as is if all that is required is launching the Tcl payload" +# -- +# -- Note that sh/bash script isn't called when running a .bat/.cmd from cmd.exe on windows by default +# -- adjust @call line above ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate +# -- if sh/bash scripting needs to run on windows too. +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin sh Payload +#printf "start of bash or sh code" + +# +# + +# -- --- --- --- --- --- --- --- +# +exitcode=0 ;#default assumption +#-- sh/bash launches Tcl here instead of shebang line at top +#-- use exec to use exitcode (if any) directly from the tcl script +#exec /usr/bin/env tclsh "$0" "$@" +#-- alternative - can run sh/bash script after the tcl call. +/usr/bin/env tclsh "$0" "$@" +exitcode=$? +#echo "tcl exitcode: ${exitcode}" +#-- override exitcode example +#exit 66 +# +# -- --- --- --- --- --- --- --- + +# +# + + +#printf "sh/bash done \n" +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end sh Payload +#------------------------------------------------------ +fi +exit ${exitcode} +# end hide sh/bash block from Tcl +# This comment with closing brace should stay in place whether if commented or not } +#------------------------------------------------------ +# begin hide powershell-block from Tcl - only needed if Tcl didn't exit or return above +if 0 { +: end heredoc1 - end hide from powershell \ +'@ +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +# -- powershell/pwsh section +# -- +# ## ### ### ### ### ### ### ### ### ### ### ### ### ### +function GetScriptName { $myInvocation.ScriptName } +$scriptname = getScriptName +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---begin powershell Payload +#"Timestamp : {0,10:yyyy-MM-dd HH:mm:ss}" -f $(Get-Date) | write-host +#"Script Name : {0}" -f $scriptname | write-host +#"Powershell Version: {0}" -f $PSVersionTable.PSVersion.Major | write-host +#"powershell args : {0}" -f ($args -join ", ") | write-host +# -- --- --- --- + +# +# + + +# -- --- --- --- --- --- --- --- +# +tclsh $scriptname $args +# +# -- --- --- --- --- --- --- --- + + +# +# + +# unbal } + +# -- --- --- --- --- --- --- --- --- --- --- --- --- ---end powershell Payload +#"powershell reporting exitcode: {0}" -f $LASTEXITCODE | write-host +Exit $LASTEXITCODE +# heredoc2 for powershell to ignore block below +$1 = @' +' +: end hide powershell-block from Tcl \ +# This comment with closing brace should stay in place whether 'if' commented or not } +: cmd exit label - return exitcode +:exit +: \ +@REM @ECHO exitcode: !task_exitcode! +: \ +@EXIT /B !task_exitcode! +# cmd has exited +: end heredoc2 \ +'@ +<# +# id:tailblock0 +# -- powershell multiline comment +#> +<# +# id:tailblock1 +# + +# +# -- unreachable by tcl directly if ctrl-z character is in the section above. (but file can be read and split on \x1A) +# -- Potential for zip and/or base64 contents, but we can't stop pwsh parser from slurping in the data +# -- so for example a plain text tar archive could cause problems depending on the content. +# -- final line in file must be the powershell multiline comment terminator or other data it can handle. +# -- e.g plain # comment lines will work too +# -- (for example a powershell digital signature is a # commented block of data at the end of the file) +#> + + diff --git a/src/modules/punk/mix/templates/utility/shellbat.txt b/src/modules/punk/mix/templates/utility/shellbat.txt index c15dd803..25c7d1d8 100644 --- a/src/modules/punk/mix/templates/utility/shellbat.txt +++ b/src/modules/punk/mix/templates/utility/shellbat.txt @@ -1,106 +1,104 @@ -if (true=="shellbat") #;#\ -: <<'HIDE_FROM_BASH_AND_SH' -::lindex tcl;# leading colons hide from .bat, trailing slash hides next line from tcl \ -@call tclsh "%~dp0%~n0.bat" %* -::lindex tcl;#\ -@set taskexitcode=%errorlevel% & goto :exit -# -*- tcl -*- -# ################################################################################################# -# This is a tcl shellbat file -# It is tuned to run when called as a batch file, a tcl script, an sh script or a bash script, -# so the specific layout and characters used are 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.sh.bat in sh or bash or powershell -# e.g filename.sh or filename.sh.bat at windows command prompt -# e.g tclsh filename.sh.bat | sh filename.sh.bat | bash filename.sh.bat -# In all cases an arbitrary number of arguments are accepted -# To avoid the initial commandline on stdout when calling as a batch file on windows, use: -# cmd /Q /c filename.sh.bat -# (because we cannot use @if to silence it, as this isn't understood by tcl,sh or bash) -# ################################################################################################# -#fconfigure stdout -translation crlf -# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload -#puts "script : [info script]" -#puts "argcount : $::argc" -#puts "argvalues: $::argv" - -# - -# --- --- --- --- --- --- --- --- --- --- --- --- --- -# only exit if needed. see exitcode notes at bottom of file and exit there for consistency across invocation methods -# --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload -#-- -#-- bash/sh code follows. -#-- protect from tcl using line continuation char on the previous comment for each line, like so: \ -printf "etc" -#-- or alternatively place sh/bash script within the false==false block -#-- whilst being careful to balance braces {} -#-- For more complex needs you should call out to external scripts -#-- -#-- END marker for hide_from_bash_and_sh\ -HIDE_FROM_BASH_AND_SH -#\ -then - -#--------------------------------------------------------- -if false==false # else { -then -: -#--------------------------------------------------------- - #-- leave as is if all that's required is launching the Tcl payload" - #-- - #-- Note that sh/bash script isn't called when running a .bat from cmd.exe on windows by default - #-- adjust line 4: @call tclsh ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate - #-- if sh/bash scripting needs to run on windows too. - #-- - #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 - if sh/bash script required to run after the tcl call. - #/usr/bin/env tclsh "$0" "$@" - #tcl_exitcode=$? - #echo "tcl_exitcode: ${tcl_exitcode}" - - #-- override exitcode example - #exit 66 - - #printf "No need for trailing slashes for sh/bash code here\n" -#--------------------------------------------------------- -fi -# } -#--------------------------------------------------------- - -#-- comment for line sample 1 with trailing continuation slash \ -#printf "tcl-invisible sh/bash line sample 1 \n" - -#-- comment for line sample 2 with trailing continuation slash \ -#printf "tcl-invisible sh/bash line sample 2 \n" - - -#-- Consistent exitcode from sh,bash,tclsh or cmd -#-- Call exit in tcl (or sh/bash) code only if explicitly required, otherwise leave this commented out. -#-- (script might be more widely useable without explicit exit. e.g in tcl: set ::argc 1; set ::argv "val"; source filename.sh.bat ) -#-- exit line unprotected by trailing slash will work for tcl and/or sh/bash -#exit 0 -#exit 42 - - -#--------------------------------------------------------- -#-- end if true==shellbat on very first line\ -fi -#--------------------------------------------------------- - -#-- make sure sh/bash/tcl all skip over .bat style exit \ -: <<'shell_end' -#-- .bat exit with exitcode from tcl process \ -:exit -::lindex tcl;#\ -@exit /B %taskexitcode% -#\ -shell_end - +: "[proc : args {}]" ;# *tcl shellbat - call with sh,bash,tclsh on any platform, or with cmd on windows. +: <<'HIDE_FROM_BASH_AND_SH' +: ;# leading colon hides from .bat, trailing slash hides next line from tcl \ +@call tclsh "%~dp0%~n0.bat" %* +: ;#\ +@set taskexitcode=%errorlevel% & goto :exit +# -*- tcl -*- +# ################################################################################################# +# This is a tcl shellbat file +# It is tuned to run when called as a batch file, a tcl script, an sh script or a bash script, +# so the specific layout and characters used are 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.sh.bat in sh or bash or powershell +# e.g filename.sh or filename.sh.bat at windows command prompt +# e.g tclsh filename.sh.bat | sh filename.sh.bat | bash filename.sh.bat +# In all cases an arbitrary number of arguments are accepted +# To avoid the initial commandline on stdout when calling as a batch file on windows, use: +# cmd /Q /c filename.sh.bat +# (because we cannot use @if to silence it, as this isn't understood by tcl,sh or bash) +# ################################################################################################# +#fconfigure stdout -translation crlf +# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" + + +# + +# --- --- --- --- --- --- --- --- --- --- --- --- --- +# only exit if needed. see exitcode notes at bottom of file and exit there for consistency across invocation methods +# --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +#-- +#-- bash/sh code follows. +#-- protect from tcl using line continuation char on the previous comment for each line, like so: \ +printf "etc" +#-- or alternatively place sh/bash script within the false==false block +#-- whilst being careful to balance braces {} +#-- For more complex needs you should call out to external scripts +#-- +#-- END marker for hide_from_bash_and_sh\ +HIDE_FROM_BASH_AND_SH + +#--------------------------------------------------------- +#-- This if statement hides(mostly) a sh/bash code block from Tcl +if false==false # else { +then +: +#--------------------------------------------------------- + #-- leave as is if all that's required is launching the Tcl payload" + #-- + #-- Note that sh/bash script isn't called when running a .bat from cmd.exe on windows by default + #-- adjust line 4: @call tclsh ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate + #-- if sh/bash scripting needs to run on windows too. + #-- + #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 - if sh/bash script required to run after the tcl call. + #/usr/bin/env tclsh "$0" "$@" + #tcl_exitcode=$? + #echo "tcl_exitcode: ${tcl_exitcode}" + + #-- override exitcode example + #exit 66 + + #printf "No need for trailing slashes for sh/bash code here\n" +#--------------------------------------------------------- +fi +# closing brace for Tcl } +#--------------------------------------------------------- + +#-- tcl and shell script now both active + +#-- comment for line sample 1 with trailing continuation slash \ +#printf "tcl-invisible sh/bash line sample 1 \n" + +#-- comment for line sample 2 with trailing continuation slash \ +#printf "tcl-invisible sh/bash line sample 2 \n" + + +#-- Consistent exitcode from sh,bash,tclsh or cmd +#-- Call exit in tcl (or sh/bash) code only if explicitly required, otherwise leave this commented out. +#-- (script might be more widely useable without explicit exit. e.g in tcl: set ::argc 1; set ::argv "val"; source filename.sh.bat ) +#-- exit line unprotected by trailing slash will work for tcl and/or sh/bash +#exit 0 +#exit 42 + + + +#-- make sure sh/bash/tcl all skip over .bat style exit \ +: <<'shell_end' +#-- .bat exit with exitcode from tcl process \ +:exit +: ;# \ +@exit /B %taskexitcode% +# .bat has exited \ +shell_end + diff --git a/src/modules/punk/mix/templates/utility/shellbat_v1.txt b/src/modules/punk/mix/templates/utility/shellbat_v1.txt new file mode 100644 index 00000000..e504ee01 --- /dev/null +++ b/src/modules/punk/mix/templates/utility/shellbat_v1.txt @@ -0,0 +1,106 @@ +if (true=="shellbat") #;#\ +: <<'HIDE_FROM_BASH_AND_SH' +::lindex tcl;# leading colons hide from .bat, trailing slash hides next line from tcl \ +@call tclsh "%~dp0%~n0.bat" %* +::lindex tcl;#\ +@set taskexitcode=%errorlevel% & goto :exit +# -*- tcl -*- +# ################################################################################################# +# This is a tcl shellbat file +# It is tuned to run when called as a batch file, a tcl script, an sh script or a bash script, +# so the specific layout and characters used are 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.sh.bat in sh or bash or powershell +# e.g filename.sh or filename.sh.bat at windows command prompt +# e.g tclsh filename.sh.bat | sh filename.sh.bat | bash filename.sh.bat +# In all cases an arbitrary number of arguments are accepted +# To avoid the initial commandline on stdout when calling as a batch file on windows, use: +# cmd /Q /c filename.sh.bat +# (because we cannot use @if to silence it, as this isn't understood by tcl,sh or bash) +# ################################################################################################# +#fconfigure stdout -translation crlf +# --- --- --- --- --- --- --- --- --- --- --- --- ---begin Tcl Payload +#puts "script : [info script]" +#puts "argcount : $::argc" +#puts "argvalues: $::argv" + +# + +# --- --- --- --- --- --- --- --- --- --- --- --- --- +# only exit if needed. see exitcode notes at bottom of file and exit there for consistency across invocation methods +# --- --- --- --- --- --- --- --- --- --- --- --- ---end Tcl Payload +#-- +#-- bash/sh code follows. +#-- protect from tcl using line continuation char on the previous comment for each line, like so: \ +printf "etc" +#-- or alternatively place sh/bash script within the false==false block +#-- whilst being careful to balance braces {} +#-- For more complex needs you should call out to external scripts +#-- +#-- END marker for hide_from_bash_and_sh\ +HIDE_FROM_BASH_AND_SH +#\ +then + +#--------------------------------------------------------- +if false==false # else { +then +: +#--------------------------------------------------------- + #-- leave as is if all that's required is launching the Tcl payload" + #-- + #-- Note that sh/bash script isn't called when running a .bat from cmd.exe on windows by default + #-- adjust line 4: @call tclsh ... to something like @call sh ... @call bash .. or @call env sh ... etc as appropriate + #-- if sh/bash scripting needs to run on windows too. + #-- + #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 - if sh/bash script required to run after the tcl call. + #/usr/bin/env tclsh "$0" "$@" + #tcl_exitcode=$? + #echo "tcl_exitcode: ${tcl_exitcode}" + + #-- override exitcode example + #exit 66 + + #printf "No need for trailing slashes for sh/bash code here\n" +#--------------------------------------------------------- +fi +# } +#--------------------------------------------------------- + +#-- comment for line sample 1 with trailing continuation slash \ +#printf "tcl-invisible sh/bash line sample 1 \n" + +#-- comment for line sample 2 with trailing continuation slash \ +#printf "tcl-invisible sh/bash line sample 2 \n" + + +#-- Consistent exitcode from sh,bash,tclsh or cmd +#-- Call exit in tcl (or sh/bash) code only if explicitly required, otherwise leave this commented out. +#-- (script might be more widely useable without explicit exit. e.g in tcl: set ::argc 1; set ::argv "val"; source filename.sh.bat ) +#-- exit line unprotected by trailing slash will work for tcl and/or sh/bash +#exit 0 +#exit 42 + + +#--------------------------------------------------------- +#-- end if true==shellbat on very first line\ +fi +#--------------------------------------------------------- + +#-- make sure sh/bash/tcl all skip over .bat style exit \ +: <<'shell_end' +#-- .bat exit with exitcode from tcl process \ +:exit +::lindex tcl;#\ +@exit /B %taskexitcode% +#\ +shell_end + diff --git a/src/modules/punk/repo-999999.0a1.0.tm b/src/modules/punk/repo-999999.0a1.0.tm index 885374a2..90ec3617 100644 --- a/src/modules/punk/repo-999999.0a1.0.tm +++ b/src/modules/punk/repo-999999.0a1.0.tm @@ -24,7 +24,15 @@ # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +# +# path/repo functions +# +package require punk::winpath +package require cksum ;#tcllib +package require fileutil; #tcllib + namespace eval punk::repo { + variable tmpfile_counter 0 ;#additional tmpfile collision avoidance proc is_fossil {{path {}}} { if {$path eq {}} { set path [pwd] } @@ -34,6 +42,20 @@ namespace eval punk::repo { if {$path eq {}} { set path [pwd] } return [expr {[find_git $path] ne {}}] } + #tracked repo - but may not be a project + proc is_repo {{path {}}} { + if {$path eq {}} { set path [pwd] } + return [expr {[isfossil] || [is_git]}] + } + proc is_candidate {{path {}}} { + if {$path eq {}} { set path [pwd] } + return [expr {[find_candidate $path] ne {}}] + } + proc is_project {{path {}}} { + if {$path eq {}} { set path [pwd] } + return [expr {[find_project $path] ne {}}] + } + proc find_fossil {{path {}}} { if {$path eq {}} { set path [pwd] } @@ -43,6 +65,37 @@ namespace eval punk::repo { if {$path eq {}} { set path [pwd] } scanup $path is_git_root } + proc find_candidate {{path {}}} { + if {$path eq {}} { set path [pwd] } + scanup $path is_candidate_root + } + proc find_repo {{path {}}} { + if {$path eq {}} { set path [pwd] } + #find the closest (lowest in dirtree) repository + set f_root [find_fossil $path] + set g_root [find_git $path] + if {[string length $f_root]} { + if {[string length $g_root]} { + if {[path_a_below_b $f_root $g_root]} { + return $f_root + } else { + return $g_root + } + } else { + return $f_root + } + } else { + if {[string length $g_root]} { + return $g_root + } else { + return "" + } + } + } + proc find_project {{path {}}} { + if {$path eq {}} { set path [pwd] } + scanup $path is_project_root + } proc is_fossil_root {{path {}}} { if {$path eq {}} { set path [pwd] } @@ -62,13 +115,64 @@ namespace eval punk::repo { set control $path/.git expr {[file exists $control] && [file isdirectory $control]} } + proc is_repo_root {{path {}}} { + if {$path eq {}} { set path [pwd] } + expr {[is_fossil_root $path] || [is_git_root $path]} + } + #require a minimum of /src and /modules - and that it's otherwise sensible + proc is_candidate_root {{path {}}} { + if {$path eq {}} { set path [pwd] } + + if {$::tcl_platform(platform) eq "windows"} { + set normpath [punk::repo::norm [punk::winpath::winpath $path]] + } else { + set normpath [punk::repo::norm $path] + } + set unwise_paths [list "/" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"] + if {[string tolower $normpath] in $unwise_paths} { + return 0 + } + if {[file pathtype [string trimright $normpath /]] eq "volumerelative"} { + #tcl 8.6/8.7 cd command doesn't preserve the windows "ProviderPath" (per drive current working directory) + return 0 + } + + #review - adjust to allow symlinks to folders? + foreach required { + src + src/lib + src/modules + lib + modules + } { + set req $path/$required + if {(![file exists $req]) || ([file type $req] ne "directory") } {return 0} + } + return 1 + } + proc is_project_root {path} { + #review - find a reliable simple mechanism. Noting we have projects based on different templates. + #Should there be a specific required 'project' file of some sort? + + #test for file/folder items indicating fossil or git workdir base + if {(![punk::repo::is_fossil_root $path]) && (![punk::repo::is_git_root $path])} { + return 0 + } + #exclude some known places we wouldn't want to put a project + if {![is_candidate_root $path]} { + return 0 + } + return 1 + } proc git_revision {{path {}}} { if {$path eq {}} { set path [pwd] } # ::kettle::path::revision.git do_in_path $path { try { - set v [::exec {*}[auto_execok git] describe] + #git describe will error with 'No names found' if repo has no tags + #set v [::exec {*}[auto_execok git] describe] + set v [::exec {*}[auto_execok git] rev-parse HEAD] ;# consider 'git rev-parse --short HEAD' } on error {e o} { set v [lindex [split [dict get $o -errorinfo] \n] 0] } @@ -90,6 +194,91 @@ namespace eval punk::repo { } } + + proc cksum_path_content {path args} { + dict set args -cksum_content 1 + dict set args -cksum_meta 0 + tailcall cksum_path $path {*}args + } + #for full cksum - using tar could reduce number of hashes to be made.. + #but as it stores metadata such as permission - we don't know if/how the archive will vary based on platform/filesystem + #-noperms only available on extraction - so that doesn't help + #Needs to operate on non-existant paths and return empty string in cksum field + proc cksum_path {path args} { + set base [file dirname [file normalize $path]] + set startdir [pwd] + + set defaults [list -cksum_content 1 -cksum_meta 1 -cksum_acls 0 -use_tar 1] + set opts [dict merge $defaults $args] + if {![file exists $path]} { + return [list cksum "" opts $opts] + } + + + + set opt_cksum_acls [dict get $opts -cksum_acls] + if {$opt_cksum_acls} { + puts stderr "cksum_path is not yet able to cksum ACLs" + return + } + set opt_cksum_meta [dict get $opts -cksum_meta] + if {$opt_cksum_meta} { + + } else { + if {[file type $path] ne "file"} { + puts stderr "cksum_path doesn't yet support a content-only cksum of a folder structure. Currently only files supported without metadata. For folders use cksum_path -cksum_meta 1" + return [list error unsupported opts $opts] + } + } + set opt_use_tar [dict get $opts -use_tar] + if {$opt_use_tar} { + package require tar ;#from tcllib + } else { + if {[file type $path] eq "directory"} { + puts stderr "cksum_path doesn't yet support -use_tar 0 for folders" + return [list error unsupported opts $opts] + } + } + + if {$path eq $base} { + #attempting to cksum at root/volume level of a filesystem.. extra work + puts stderr "cksum_path doesn't yet support cksum of entire volume. (todo)" + return [list error unsupported opts $opts] + } + set cksum "" + if {$opt_use_tar} { + set target [file tail $path] + set tmplocation [tmpdir] + set archivename $tmplocation/[tmpfile].tar + + cd $base ;#cd is process-wide.. keep cd in effect for as small a scope as possible. (review for thread issues) + + #temp emission to stdout.. todo - repl telemetry channel + puts stdout "cksum_path: creating temporary tar archive at: $archivename .." + tar::create $archivename $target + puts stdout "cksum_path: calculating cksum for $target (size [file size $target])..." + set cksum [crc::cksum -format 0x%X -file $archivename] + puts stdout "cksum_path: cleaning up.. " + file delete -force $archivename + cd $startdir + + } else { + #todo + if {[file type $path] eq "file"} { + if {$opt_cksum_meta} { + return [list error unsupported opts $opts] + } else { + set cksum [crc::cksum -format 0x%X -file $path] + } + } else { + error "cksum_path unsupported $opts for path type [file type $path]" + } + } + set result [dict create] + dict set result cksum $cksum + dict set result opts $opts + return $result + } #temporarily cd to workpath to run script - return to correct path even on failure proc do_in_path {path script} { #from ::kettle::path::in @@ -121,7 +310,10 @@ namespace eval punk::repo { } return {} } - + #get content part of content/zip delimited by special \x1a (ctrl-z) char as used in tarjr and kettle::path::c/z + proc c/z {content} { + return [lindex [split $content \x1A] 0] + } proc grep {pattern data} { set data [string map [list \r\n \n] $data] return [lsearch -all -inline -glob [split $data \n] $pattern] @@ -132,6 +324,230 @@ namespace eval punk::repo { return [lsearch -all -inline -regexp [split $data \n] $pattern] } + proc tmpfile {{prefix tmp_}} { + #note risk of collision if pregenerating a list of tmpfile names + #we will maintain an icrementing id so the caller doesn't have to bear that in mind + variable tmpfile_counter + global tcl_platform + return .punkrepo_$prefix[pid]_[clock microseconds]_[incr tmpfile_counter]_[info hostname]_$tcl_platform(user) + } + + proc tmpdir {} { + # Taken from tcllib fileutil. + global tcl_platform env + + set attempdirs [list] + set problems {} + + foreach tmp {TMPDIR TEMP TMP} { + if { [info exists env($tmp)] } { + lappend attempdirs $env($tmp) + } else { + lappend problems "No environment variable $tmp" + } + } + + switch $tcl_platform(platform) { + windows { + lappend attempdirs "C:\\TEMP" "C:\\TMP" "\\TEMP" "\\TMP" + } + macintosh { + lappend attempdirs $env(TRASH_FOLDER) ;# a better place? + } + default { + lappend attempdirs \ + [file join / tmp] \ + [file join / var tmp] \ + [file join / usr tmp] + } + } + + lappend attempdirs [pwd] + + foreach tmp $attempdirs { + if { [file isdirectory $tmp] && + [file writable $tmp] } { + return [file normalize $tmp] + } elseif { ![file isdirectory $tmp] } { + lappend problems "Not a directory: $tmp" + } else { + lappend problems "Not writable: $tmp" + } + } + + # Fail if nothing worked. + return -code error "Unable to determine a proper directory for temporary files\n[join $problems \n]" + } + + #todo - review + proc ensure-cleanup {path} { + #::atexit [lambda {path} { + #file delete -force $path + #} [norm $path]] + + file delete -force $path + } + + proc path_relative {base dst} { + #see also kettle + # Modified copy of ::fileutil::relative (tcllib) + # Adapted to 8.5 ({*}). + # + # Taking two _directory_ paths, a base and a destination, computes the path + # of the destination relative to the base. + # + # Arguments: + # base The path to make the destination relative to. + # dst The destination path + # + # Results: + # The path of the destination, relative to the base. + + # Ensure that the link to directory 'dst' is properly done relative to + # the directory 'base'. + + if {[file pathtype $base] ne [file pathtype $dst]} { + return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)" + } + + set base [norm $base] + set dst [norm $dst] + + set save $dst + set base [file split $base] + set dst [file split $dst] + + while {[lindex $dst 0] eq [lindex $base 0]} { + set dst [lrange $dst 1 end] + set base [lrange $base 1 end] + if {![llength $dst]} {break} + } + + set dstlen [llength $dst] + set baselen [llength $base] + + if {($dstlen == 0) && ($baselen == 0)} { + # Cases: + # (a) base == dst + + set dst . + } else { + # Cases: + # (b) base is: base/sub = sub + # dst is: base = {} + + # (c) base is: base = {} + # dst is: base/sub = sub + + while {$baselen > 0} { + set dst [linsert $dst 0 ..] + incr baselen -1 + } + set dst [file join {*}$dst] + } + + return $dst + } + + #literate-programming style naming for some path tests + #Note the naming of the operator portion of a_op_b is consistent in that it is the higher side of the filesystem tree first. + #hence aboveorat vs atorbelow + #These names also sort in the logical order of higher to lower in the filesystem (when considering the root as 'higher' in the filesystem) + proc path_a_above_b {path_a path_b} { + #stripPath prefix path + return [expr {[fileutil::stripPath $path_a $path_b] ni [list . $path_b]}] + } + proc path_a_aboveorat_b {path_a path_b} { + return [expr {[fileutil::stripPath $path_a $path_b] ne $path_b}] + } + proc path_a_at_b {path_a path_b} { + return [expr {[fileutil::stripPath $path_a $path_b] eq "." }] + } + proc path_a_atorbelow_b {path_a path_b} { + return [expr {[fileutil::stripPath $path_b $path_a] ne $path_a}] + } + proc path_a_below_b {path_a path_b} { + return [expr {[fileutil::stripPath $path_b $path_a] ni [list . $path_a]}] + } + proc path_a_inlinewith_b {path_a path_b} { + return [expr {[path_a_aboveorat_b $path_a $path_b] || [path_a_below_b $path_a $path_b]}] + } + + + proc norm {path {platform env}} { + #kettle::path::norm + #see also wiki + #full path normalization + + set platform [string tolower $platform] + if {$platform eq "env"} { + set platform $::tcl_platform(platform) + } + if {$platform eq "windows"} { + return [file dirname [file normalize [punk::winpath::winpath $path]/__]] + } else { + return [file dirname [file normalize $path/__]] + } + } + + #This taken from kettle::path::strip + #It doesn't compare the prefix contents presumably for speed when used in kettle::path::scan + #renamed to better indicate its behaviour + proc path_strip_prefixdepth {path prefix} { + return [file join \ + {*}[lrange \ + [file split [norm $path]] \ + [llength [file split [norm $prefix]]] \ + end]] + } + + proc fcat {args} { + if {$::tcl_platform(platform) ne "windows"} { + return [fileutil::cat {*}$args] + } + + set knownopts [list -eofchar -translation -encoding --] + set last_opt 0 + for {set i 0} {$i < [llength $args]} {incr i} { + set ival [lindex $args $i] + #puts stdout "i:$i a: $ival known: [expr {$ival in $knownopts}]" + if {$ival eq "--"} { + set last_opt $i + break + } else { + if {$ival in $knownopts} { + #puts ">known at $i : [lindex $args $i]" + if {($i % 2) != 0} { + error "unexpected option at index $i. known options: $knownopts must come in -opt val pairs." + } + incr i + set last_opt $i + } else { + set last_opt [expr {$i - 1}] + break + } + } + } + set first_non_opt [expr {$last_opt + 1}] + + #puts stderr "first_non_opt: $first_non_opt" + set opts [lrange $args -1 $first_non_opt-1] + set paths [lrange $args $first_non_opt end] + if {![llength $paths]} { + error "Unable to find file in the supplied arguments: $args. Ensure options are all -opt val pairs and that file name(s) follow" + } + #puts stderr "opts: $opts paths: $paths" + set finalpaths [list] + foreach p $paths { + if {[punk::winpath::illegalname_test $p]} { + lappend finalpaths [punk::winpath::illegalname_fix $p] + } else { + lappend finalpaths $p + } + } + fileutil::cat {*}$opts {*}$finalpaths + } + interp alias {} is_fossil {} ::punk::repo::is_fossil interp alias {} is_fossil_root {} ::punk::repo::is_fossil_root interp alias {} find_fossil {} ::punk::repo::find_fossil diff --git a/src/modules/punk/winpath-999999.0a1.0.tm b/src/modules/punk/winpath-999999.0a1.0.tm index 067311d9..b987264a 100644 --- a/src/modules/punk/winpath-999999.0a1.0.tm +++ b/src/modules/punk/winpath-999999.0a1.0.tm @@ -59,8 +59,10 @@ namespace eval punk::winpath { if {[regexp $re_slash_x_slash $path _ letter]} { #upper case appears to be windows canonical form set path [string toupper $letter]:/[string range $path 3 end] - } elseif {[regexp {^/mnt|MNT/([[:alpha:]]){1}/.*} $path _ letter]} { + } elseif {[regexp {^/mnt/([[:alpha:]]){1}/.*} [string tolower $path] _ letter]} { set path [string toupper $letter]:/[string range $path 7 end] + } elseif {[regexp {^/mnt/([[:alpha:]]){1}$} [string tolower $path] _ letter]} { + set path [string toupper $letter]:/ } elseif {[regexp $re_slash_else $path _ firstpart remainder]} { #could be for example /c or /something/users if {[string length $firstpart] == 1} { diff --git a/src/scriptapps/dtplite.tcl b/src/scriptapps/dtplite.tcl new file mode 100644 index 00000000..e78827cd --- /dev/null +++ b/src/scriptapps/dtplite.tcl @@ -0,0 +1,28 @@ +#! /usr/bin/env tclsh +# -*- tcl -*- + +# @@ Meta Begin +# Application dtplite 1.0.5 +# Meta platform tcl +# Meta summary Lightweight DocTools Processor +# Meta description This application is a simple processor +# Meta description for documents written in the doctools +# Meta description markup language. It covers the most +# Meta description common use cases, but is not as +# Meta description configurable as its big brother dtp. +# Meta category Processing doctools documents +# Meta subject doctools doctoc docidx +# Meta require {dtplite 1.0.5} +# Meta author Andreas Kupries +# Meta license BSD +# @@ Meta End + +package require dtplite 1.0.5 + +# dtp lite - Lightweight DocTools Processor +# ======== = ============================== + +exit [dtplite::do $argv] + +# ### ### ### ######### ######### ######### +exit diff --git a/src/fetchruntime.ps1 b/src/scriptapps/fetchruntime.ps1 similarity index 100% rename from src/fetchruntime.ps1 rename to src/scriptapps/fetchruntime.ps1 diff --git a/src/deps/dictutils-0.2.tm b/src/vendormodules/dictutils-0.2.tm similarity index 100% rename from src/deps/dictutils-0.2.tm rename to src/vendormodules/dictutils-0.2.tm diff --git a/src/deps/metaface-1.2.5.tm b/src/vendormodules/metaface-1.2.5.tm similarity index 100% rename from src/deps/metaface-1.2.5.tm rename to src/vendormodules/metaface-1.2.5.tm diff --git a/src/vendormodules/natsort-0.1.1.5.tm b/src/vendormodules/natsort-0.1.1.5.tm new file mode 100644 index 00000000..d1a3bdb7 --- /dev/null +++ b/src/vendormodules/natsort-0.1.1.5.tm @@ -0,0 +1,1883 @@ +#! /usr/bin/env tclsh + +package provide natsort [namespace eval natsort { + variable version + set version 0.1.1.5 +}] + +package require flagfilter +namespace import ::flagfilter::check_flags + +namespace eval natsort { + proc scriptdir {} { + set possibly_linked_script [file dirname [file normalize [file join [info script] ...]]] + if {[file isdirectory $possibly_linked_script]} { + return $possibly_linked_script + } else { + return [file dirname $possibly_linked_script] + } + } + tcl::tm::add [scriptdir] +} + + +namespace eval natsort { + variable stacktrace_on 0 + + proc do_error {msg {then error}} { + #note we exit or error out even if debug selected - as every do_error call is meant to interrupt code processing at the site of call + #this is not just a 'logging' call even though it has log-like descriptors + lassign $then type code + if {$code eq ""} { + set code 1 + } + set type [string tolower $type] + set levels [list debug info notice warn error critical] + if {$type in [concat $levels exit]} { + puts stderr "|$type> $msg" + } else { + puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be one of '$levels' or 'exit '" + } + if {$::tcl_interactive} { + #may not always be desirable - but assumed to be more useful not to exit despite request, to aid in debugging + if {[string tolower $type] eq "exit"} { + puts stderr " (exit suppressed due to tcl_interactive - raising error instead)" + if {![string is digit -strict $code]} { + puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be: 'exit '" + } + } + return -code error $msg + } else { + if {$type ne "exit"} { + return -code error $msg + } else { + if {[string is digit -strict $code]} { + exit $code + } else { + puts stderr "|>natsort_call_err> unable to interpret 2nd argument to do_error: '$then' should be 'error' or 'exit '" + return -code error $msg + } + } + } + } + + + + + + + variable debug 0 + variable testlist + set testlist { + 00.test-firstposition.txt + 0001.blah.txt + 1.test-sorts-after-all-leadingzero-number-one-equivs.txt + 1010.thousand-and-ten.second.txt + 01010.thousand-and-ten.first.txt + 0001.aaa.txt + 001.zzz.txt + 08.octal.txt-last-octal + 008.another-octal-first-octal.txt + 08.again-second-octal.txt + 001.a.txt + 0010.reconfig.txt + 010.etc.txt + 005.etc.01.txt + 005.Etc.02.txt + 005.123.abc.txt + 200.somewhere.txt + 2zzzz.before-somewhere.txt + 00222-after-somewhere.txt + 005.00010.abc.txt + 005.a3423bc.00010.abc.txt + 005.001.abc.txt + 005.etc.1010.txt + 005.etc.010.txt + 005.etc.10.txt + " 005.etc.10.txt" + 005.etc.001.txt + 20.somewhere.txt + 4611686018427387904999999999-bignum.txt + 4611686018427387903-bigishnum.txt + 9223372036854775807-bigint.txt + etca-a + etc-a + etc2-a + a0001blah.txt + a010.txt + winlike-sort-difference-0.1.txt + winlike-sort-difference-0.1.1.txt + a1.txt + b1-a0001blah.txt + b1-a010.txt + b1-a1.txt + -a1.txt + --a1.txt + --a10.txt + 2.high-two.yml + 02.higher-two.yml + reconfig.txt + _common.stuff.txt + CASETEST.txt + casetest.txt + something.txt + some~thing.txt + someathing.txt + someThing.txt + thing.txt + thing_revised.txt + thing-revised.txt + "thing revised.txt" + "spacetest.txt" + " spacetest.txt" + " spacetest.txt" + "spacetest2.txt" + "spacetest 2.txt" + "spacetest02.txt" + name.txt + name2.txt + "name .txt" + "name2 .txt" + blah.txt + combined.txt + a001.txt + .test + .ssh + "Feb 10.txt" + "Feb 8.txt" + 1ab23v23v3r89ad8a8a8a9d.txt + "Folder (10)/file.tar.gz" + "Folder/file.tar.gz" + "Folder (1)/file (1).tar.gz" + "Folder (1)/file.tar.gz" + "Folder (01)/file.tar.gz" + "Folder1/file.tar.gz" + "Folder(1)/file.tar.gz" + + } + lappend testlist "Some file.txt" + lappend testlist " Some extra file1.txt" + lappend testlist " Some extra file01.txt" + lappend testlist " some extra file1.txt" + lappend testlist " Some extra file003.txt" + lappend testlist " Some file.txt" + lappend testlist "Some extra file02.txt" + lappend testlist "Program Files (x86)" + lappend testlist "01999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999991-bigger-pathologically-bignum.txt" + lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt" + lappend testlist "29999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-smaller-pathologically-bignum.txt" + lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 1.txt" + lappend testlist "199999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999-pathologically-bignum.txt with (more 01.txt" + lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt" + lappend testlist "a1a1a1a1a1a1a1a1a1a1a1a01a1a1a1a1a1a1a1a1a1a1a1a1.pathological-num-nonnum-swapping-leadzero-should-be-first.txt" + lappend testlist "b1b1b1b1.txt" + lappend testlist "b1b01z1z1.txt" + lappend testlist "c1c111c1.txt" + lappend testlist "c1c1c1c1.txt" + + namespace eval overtype { + proc right {args} { + # @d !todo - implement overflow, length checks etc + + if {[llength $args] < 2} { + error {usage: ?-overflow [1|0]? undertext overtext} + } + foreach {undertext overtext} [lrange $args end-1 end] break + + set opt(-overflow) 0 + array set opt [lrange $args 0 end-2] + + + set olen [string length $overtext] + set ulen [string length $undertext] + + if {$opt(-overflow)} { + return [string range $undertext 0 end-$olen]$overtext + } else { + if {$olen > $ulen} { + set diff [expr {$olen - $ulen}] + return [string range $undertext 0 end-$olen][string range $overtext 0 end-$diff] + } else { + return [string range $undertext 0 end-$olen]$overtext + } + } + } + proc left {args} { + # @c overtype starting at left (overstrike) + # @c can/should we use something like this?: 'format "%-*s" $len $overtext + + if {[llength $args] < 2} { + error {usage: ?-overflow [1|0]? ?-ellipsis [1|0]? ?-ellipsistext ...? undertext overtext} + } + foreach {undertext overtext} [lrange $args end-1 end] break + + set opt(-ellipsis) 0 + set opt(-ellipsistext) {...} + set opt(-overflow) 0 + array set opt [lrange $args 0 end-2] + + + set len [string length $undertext] + set overlen [string length $overtext] + set diff [expr {$overlen - $len}] + + #puts stdout "====================>overtype: datalen:$len overlen:$overlen diff:$diff" + #puts stdout "====================>overtype: data: $overtext" + if {$diff > 0} { + if {$opt(-overflow)} { + return $overtext + } else { + if {$opt(-ellipsis)} { + return [overtype::left [string range $overtext 0 [expr {$len -1}]] $opt(-ellipsistext)] + } else { + return [string range $overtext 0 [expr {$len -1}]] + } + } + } else { + return "$overtext[string range $undertext $overlen end]" + } + } + + } + + #considered using hex to make large numbers more compact for viewing in debug output - but it's not that much shorter and probably obscures more than it helps. + proc hex2dec {largeHex} { + set res 0 + foreach hexDigit [split $largeHex {}] { + set new 0x$hexDigit + set res [expr {16*$res + $new}] + } + return $res + } + proc dec2hex {decimalNumber} { + format %4.4llX $decimalNumber + } + proc trimzero {number} { + set trimmed [string trimleft $number 0] + if {[string length $trimmed] == 0} { + set trimmed 0 + } + return $trimmed + } + #todo - consider human numeric split + #e.g consider SI suffixes k|KMGTPEZY in that order + + proc split_numeric_segments {name} { + set segments [list] + while {[string length $name]} { + if {[scan $name {%[0-9]%n} chunk len] == 2} { + lappend segments $chunk + set name [string range $name $len end] + } + if {[scan $name {%[^0-9]%n} chunk len] == 2} { + lappend segments $chunk + set name [string range $name $len end] + } + } + return $segments + } + + proc padleft {str count {ch " "}} { + set val [string repeat $ch $count] + append val $str + set diff [expr {max(0,$count - [string length $str])}] + set offset [expr {max(0,$count - $diff)}] + set val [string range $val $offset end] + } + + + # Sqlite may have limited collation sequences available in default builds. + # with custom builds - there may be others such as 'natsort' - see https://sqlite.org/forum/forumpost/e4dc6f3331 + # This is of limited use with the few builtin collations available in 2023 ie binary,nocase & rtrim + # but may provide a quicker,flexible sort option, especially if/when more collation sequences are added to sqlite + # There are also prebuilt packages such as sqlite3-icu which allows things like "SELECT icu_load_collation('en_AU', 'australian');" + proc sort_sqlite {stringlist args} { + package require sqlite3 + + + set args [check_flags -caller natsort_sqlite -defaults [list -db :memory: -collate nocase -winlike 0 -topchars "\uFFFF" -debug 0 -splitchars [list / . - _] -extras {all}] -values $args] + set db [string trim [dict get $args -db]] + set collate [string trim [dict get $args -collate]] + set debug [string trim [dict get $args -debug]] + set topchars [string trim [dict get $args -topchars]] + + set topdot [expr {"." in $topchars}] + set topunderscore [expr {"_" in $topchars}] + + + sqlite3 db_sort_basic $db + set orderedlist [list] + db_sort_basic eval [string map [list %collate% $collate] {create table sqlitesort(index0 text COLLATE %collate%, name text COLLATE %collate%)}] + foreach nm $stringlist { + set segments [split_numeric_segments $nm] + set index "" + set s 0 + foreach seg $segments { + if {($s == 0) && ![string length [string trim $seg]]} { + #don't index leading space + } elseif {($s == 0) && ($topunderscore) && [string match _* [string trim $seg]]} { + append index "[padleft "0" 5]-d -100 topunderscore " + append index [string trim $seg] + } elseif {($s == 0) && ($topdot) && [string match .* [string trim $seg]]} { + append index "[padleft "0" 5]-d -50 topdot " + append index [string trim $seg] + } else { + if {[string is digit [string trim $seg]]} { + set basenum [trimzero [string trim $seg]] + set lengthindex "[padleft [string length $basenum] 5]-d" + append index "$lengthindex " + #append index [padleft $basenum 40] + append index $basenum + } else { + append index [string trim $seg] + } + } + incr s + } + puts stdout ">>$index" + db_sort_basic eval {insert into sqlitesort values($index,$nm)} + } + db_sort_basic eval [string map [list %collate% $collate] {select name from sqlitesort order by index0 COLLATE %collate% ASC, name COLLATE %collate% ASC }] { + lappend orderedlist $name + } + db_sort_basic close + return $orderedlist + } + + proc get_leading_char_count {str char} { + #todo - something more elegant? regex? + set count 0 + foreach c [split $str "" ] { + if {$c eq $char} { + incr count + } else { + break + } + } + return $count + } + proc stacktrace {} { + set stack "Stack trace:\n" + for {set i 1} {$i < [info level]} {incr i} { + set lvl [info level -$i] + set pname [lindex $lvl 0] + append stack [string repeat " " $i]$pname + + if {![catch {info args $pname} pargs]} { + foreach value [lrange $lvl 1 end] arg $pargs { + + if {$value eq ""} { + if {$arg != 0} { + info default $pname $arg value + } + } + append stack " $arg='$value'" + } + } else { + append stack " !unknown vars for $pname" + } + + append stack \n + } + return $stack + } + + proc get_char_count {str char} { + expr {[string length $str]-[string length [string map [list $char {}] $str]]} + } + + proc build_key {chunk splitchars topdict tagconfig debug} { + variable stacktrace_on + if {$stacktrace_on} { + puts stderr "+++>[stacktrace]" + } + + set index_map [list - "" _ ""] + #e.g - need to maintain the order + #a b.txt + #a book.txt + #ab.txt + #abacus.txt + + + set original_splitchars [dict get $tagconfig original_splitchars] + + # tag_dashes test moved from loop - review + set tag_dashes 0 + if {![string length [dict get $tagconfig last_part_text_tag]]} { + #winlike + set tag_dashes 1 + } + if {("-" ni $original_splitchars)} { + set tag_dashes 1 + } + if {$debug >= 3} { + puts stdout "START build_key chunk : $chunk" + puts stdout "START build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes" + } + + + ## index_map will have no effect if we've already split on the char anyway(?) + #foreach m [dict keys $index_map] { + # if {$m in $original_splitchars} { + # dict unset index_map $m + # } + #} + + #if {![string length $chunk]} return + + set result "" + if {![llength $splitchars]} { + #no more structural splits - but we need to examine numeric/non-numeric segments at the lowest level. + # we are at a leaf in the recursive split hierarchy + + set s "" ;#we never actually split on "" (unless that was put in splitchars.. but it probably shouldn't be) + set parts [list $chunk] ;#important to treat as list or leading/trailing whitespace lost + + + } else { + set s [lindex $splitchars 0] + if {"spudbucket$s" in "[split $chunk {}]"} { + error "dead-branch spudbucket" + set partindex [build_key $chunk [lrange $splitchars 1 end] $topdict $tagconfig $debug] + if {[dict get $tagconfig showsplits]} { + set pfx "(1${s}=)" ;# = sorts before _ + set partindex ${pfx}$partindex + } + + return $partindex + } else { + set parts_below_index "" + + if {$s ni [split $chunk ""]} { + #$s can be an empty string + set parts [list $chunk] + } else { + set parts [split $chunk $s] ;#whitespace preserved - even if splitting on s that is not in string. + } + #assert - we have a splitchar $s that is in the chunk - so at least one part + if {(![string length $s] || [llength $parts] == 0)} { + error "buld_key assertion false empty split char and/or no parts" + } + + set pnum 1 ;# 1 based for clarity of reading index in debug output + set subpart_count [llength $parts] + + set sub_splits [lrange $splitchars 1 end] ;#pass same splitchars to each subpart + foreach p $parts { + set partindex [build_key $p $sub_splits $topdict $tagconfig $debug] + set lastpart [expr {$pnum == $subpart_count}] + + + ####################### + set showsplits [dict get $tagconfig showsplits] + #split prefixing experiment - maybe not suitable for general use - as it affects sort order + #note that pfx must be consistent until last one, no matter how many partnumbers there are in total. + # we don't want to influence sort order before reaching end. + #e.g for: + #(1.=)... + #(1._)...(2._)...(3.=) + #(1._)...(2.=) + #Note that this is probably more suitable for highly structure dependant sorts where the results are maybe less.. natural. + if {$showsplits} { + if {$lastpart} { + set pfx "(${pnum}${s}_" + #set pfx "(${pnum}${s}=)" ;# = sorts before _ + } else { + set pfx "(${pnum}${s}_" + } + append parts_below_index $pfx + } + ####################### + + if {$lastpart} { + if {[string length $p] && [string is digit $p]} { + set last_part_tag "<22${s}>" + } else { + set last_part_tag "<33${s}>" + } + + set last_part_text_tag [dict get $tagconfig last_part_text_tag] + #for -winlike 1 there is no tag configured. Windows explorer likes to put things in the order: + # module-0.1.1.tm + # module-0.1.1.2.tm + # module-0.1.tm + # arguably -winlike 0 is more natural/human + # module-0.1.tm + # module-0.1.1.tm + # module-0.1.1.2.tm + + if {[string length $last_part_text_tag]} { + #replace only the first text-tag (<30>) from the subpart_index + if {[string match "<30?>*" $partindex]} { + #give textual string index a specific tag for last part in split only. e.g <130> for lower than integers + set partindex "<130>[string range $partindex 5 end]" + } + #append parts_below_index $last_part_tag + } + #set partindex $last_part_tag$partindex + + + } + append parts_below_index $partindex + + + + if {$showsplits} { + if {$lastpart} { + set suffix "${pnum}${s}=)" ;# = sorts before _ + } else { + set suffix "${pnum}${s}_)" + } + append parts_below_index $suffix + } + + + incr pnum + } + append parts_below_index "" ;# don't add anything at the tail that may perturb sort order + + if {$debug >= 3} { + set pad [string repeat " " 20] + puts stdout "END build_key chunk : $chunk " + puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes" + puts stdout "END build_key ret below_index: $parts_below_index" + } + return $parts_below_index + + + } + } + + + + #puts stdout ">>>chunk:'$chunk'<<< split-on:$s parts: '$parts' splitchars: $splitchars -topdict:$topdict" + + + + + + #if {$chunk eq ""} { + # puts "___________________________________________!!!____" + #} + #puts stdout "-->chunk:$chunk $s parts:$parts" + + #puts stdout "---chunk:'$chunk' part:'$part' parts:'$parts' s:'$s'" + + + + + set segments [split_numeric_segments $chunk] ;#! + set stringindex "" + set segnum 0 + foreach seg $segments { + #puts stdout "=================---->seg:$seg segments:$segments" + #-strict ? + if {[string length $seg] && [string is digit $seg]} { + set basenum [trimzero [string trim $seg]] + set lengthindex "[padleft [string length $basenum] 4]d" + #append stringindex "<20>$lengthindex $basenum $seg" + } else { + set c1 [string range $seg 0 0] + #puts stdout "==============> c1'$c1' topdict: $topdict stringindex:$stringindex" + + if {$c1 in [dict keys $topdict]} { + set tag [dict get $topdict $c1] + #append stringindex "${tag}$c1" + #set seg [string range $seg 1 end] + } + #textindex + set leader "<30>" + set idx $seg + set idx [string trim $idx] + set idx [string tolower $idx] + set idx [string map $index_map $idx] + + + + + + #set the X-c count to match the length of the index - not the raw data + set lengthindex "[padleft [string length $idx] 4]c" + + #append stringindex "${leader}$idx $lengthindex $texttail" + } + } + + if {[llength $parts] != 1} { + error "build_key assertion fail llength parts != 1 parts:$parts" + } + + set segtail_clearance_buffer " " ;#space to clear other split indicators if using showsplits + set segtail $segtail_clearance_buffer + append segtail "\[" + set grouping "" + set pnum 0 + foreach p $parts { + set sublen_list [list] + set subsegments [split_numeric_segments $p] + set i 0 + + set partsorter "" + foreach sub $subsegments { + ##don't trim spaces here - it would be inconsistent. Some subs are pure whitespace - others have internal whitespace. e.g "a4 400b a b2" becomes "a 4 { } 400 {b a b} 2" + #mapping away all whitespace would be consistent, but not necessarily desirable. If it's in the index_map it'll happen anyway - so we don't do it manually here except for evaluating conditions. + set test_trim [string trim $sub] + set str $sub + set str [string tolower $str] + set str [string map $index_map $str] + if {[string length $test_trim] && [string is digit $test_trim]} { + append partsorter [trimzero $str] + } else { + append partsorter "$str" + } + append partsorter + } + + + foreach sub $subsegments { + + if {[string length $sub] && [string is digit $sub]} { + set basenum [trimzero [string trim $sub]] + set subequivs $basenum + set lengthindex "[padleft [string length $subequivs] 4]d " + set idx "$lengthindex [padleft $basenum 10]" ;#todo - cycle through data and determine longest + set tail [overtype::left [string repeat " " 10] $sub] + #set tail "" + } else { + set idx "" + + + set lookahead [lindex $subsegments $i+1] + if {![string length $lookahead]} { + set zeronum "[padleft 0 4]d0" + } else { + set zeronum "" + } + set subequivs $sub + #set subequivs [string trim $subequivs] + set subequivs [string tolower $subequivs] + set subequivs [string map $index_map $subequivs] + + append idx $subequivs + append idx $zeronum + + set idx $subequivs + + + # + + set ch "-" + if {$tag_dashes} { + #puts stdout "____TAG DASHES" + #winlike + set numleading [get_leading_char_count $seg $ch] + if {$numleading > 0} { + set texttail "<31-leading[padleft $numleading 4]$ch>" + } else { + set texttail "<30>" + } + set numothers [expr {[get_char_count $seg $ch] - $numleading}] + if {$debug >= 2} { + puts stdout "____dashcount: [get_char_count $seg $ch] numothers: $numothers" + } + if {$numothers > 0} { + append texttail "<31-others[padleft $numothers 4]$ch>" + } else { + append textail "<30>" + } + } else { + set texttail "<30>" + } + + + + + #set idx $partsorter + set tail "" + #set tail [string tolower $sub] ;#raw + #set tail $partsorter + #append tail ":[string tolower $p]" ;#we need the whole part - even though it makes the index much larger. !todo - tagconfig switch to toggle case sensitive sorting + } + + append grouping "$idx $tail|$s" + incr i + } + + + + + + if {$p eq ""} { + # no subsegments.. + set zeronum "[padleft 0 4]d0" + #append grouping "\u000$zerotail" + append grouping ".$zeronum" + } + + #append grouping | + #append grouping $s + #foreach len $sublen_list { + # append segtail "<[padleft $len 3]>" + #} + incr pnum + } + set grouping [string trimright $grouping $s] + append grouping "[padleft [llength $parts] 4]" + append segtail $grouping + + + #append segtail " <[padleft [llength $parts] 4]>" + + append segtail "\]" + + + #if {[string length $seg] && [string is digit $seg]} { + # append segtail "<20>" + #} else { + # append segtail "<30>" + #} + append stringindex $segtail + + incr segnum + + + + + lappend indices $stringindex + + if {[llength $indices] > 1} { + puts stderr "INDICES [llength $indices]: $stringindex" + error "build_key assertion error deadconcept indices" + } + + #topchar handling on splitter characters + #set c1 [string range $chunk 0 0] + if {$s in [dict keys $topdict]} { + set tag [dict get $topdict $s] + set joiner [string map [list ">" "$s>"] ${tag}] + #we have split on this character $s so if the first part is empty string then $s was a leading character + # we need to bring a tag out front for this, or it will be dominated by the leading sections-remaing tag + # (since the empty string produces no tag of it's own - ?) + if {[string length [lindex $parts 0]] == 0} { + set prefix ${joiner} + } else { + set prefix "" + } + } else { + #use standard character-data positioning tag if no override from topdict + set joiner "<30J>$s" + set prefix "" + } + + + set contentindex $prefix[join $indices $joiner] + if {[string length $s]} { + set split_indicator "" + } else { + set split_indicator "" + + } + if {![string length $s]} { + set s ~ + } + + #return "[overtype::left [string repeat { } 80] $contentindex][overtype::left [string repeat { } 10] [list $s $chunk]]" + #return $contentindex$split_indicator + #return [overtype::left [string repeat - 40] $contentindex] + + if {$debug >= 3} { + puts stdout "END build_key chunk : $chunk" + puts stdout "END build_key splitchars : $splitchars $topdict $tagconfig NO tag dashes" + puts stdout "END build_key ret contentidx : $contentindex" + } + return $contentindex + } + + #---------------------------------------- + #line-processors - data always last argument - opts can be empty string + #all processor should accept empty opts and ignore opts if they don't use them + proc _lineinput_as_tcl1 {opts line} { + set out "" + foreach i $line { + append out "$i " + } + set out [string range $out 0 end-1] + return $out + } + #should be equivalent to above + proc _lineinput_as_tcl {opts line} { + return [concat {*}$line] + } + #will put extra tcl quoting if it was already tcl-shaped e.g text "a" -> {"a"} + proc _lineoutput_as_tcl {opts line} { + return [regexp -inline -all {\S+} $line] + } + + proc _lineinput_as_raw {opts line} { + return $line + } + proc _lineoutput_as_raw {opts line} { + return $line + } + + #words is opposite of tcl + proc _lineinput_as_words {opts line} { + #wordlike_parts + return [regexp -inline -all {\S+} $line] + } + proc _lineoutput_as_words {opts line} { + return [concat {*}$line] + } + + #opts same as tcllib csv::split - except without the 'line' element + #?-alternate? ?sepChar? ?delChar? + proc _lineinput_as_csv {opts line} { + package require csv + if {[lindex $opts 0] eq "-alternate"} { + return [csv::split -alternate $line {*}[lrange $opts 1 end]] + } else { + return [csv::split $line {*}$opts] + } + } + #opts same as tcllib csv::join + #?sepChar? ?delChar? ?delMode? + proc _lineoutput_as_csv {opts line} { + package require csv + return [csv::join $line {*}$opts] + } + #---------------------------------------- + proc sort {stringlist args} { + #puts stdout "natsort::sort args: $args" + variable debug + if {![llength $stringlist]} return + + #allow pass through of the check_flags flag -debugargs so it can be set by the caller + set debugargs 0 + if {[set posn [lsearch $args -debugargs]] >=0} { + if {$posn == [llength $args]-1} { + #-debugargs at tail of list + set debugargs 1 + } else { + set debugargs [lindex $args $posn+1] + } + } + + #-return flagged|defaults doesn't work Review. + #flagfilter global processor/allocator not working 2023-08 + set args [check_flags \ + -caller natsort::sort \ + -return supplied|defaults \ + -debugargs $debugargs \ + -defaults [list -collate nocase \ + -winlike 0 \ + -splits "\uFFFF" \ + -topchars {. _} \ + -showsplits 1 \ + -sortmethod ascii \ + -collate "\uFFFF" \ + -inputformat raw \ + -inputformatapply {index data} \ + -inputformatoptions "" \ + -outputformat raw \ + -outputformatoptions "" \ + -cols "\uFFFF" \ + -debug 0 -db "" -stacktrace 0 -splits "\uFFFF" -showsplits 0] \ + -required {all} \ + -extras {none} \ + -commandprocessors {} \ + -values $args] + + #csv unimplemented + + set winlike [dict get $args -winlike] + set topchars [dict get $args -topchars] + set cols [dict get $args -cols] + set debug [dict get $args -debug] + set stacktrace [dict get $args -stacktrace] + set showsplits [dict get $args -showsplits] + set splits [dict get $args -splits] + set sortmethod [dict get $args -sortmethod] + set opt_collate [dict get $args -collate] + set opt_inputformat [dict get $args -inputformat] + set opt_inputformatapply [dict get $args -inputformatapply] + set opt_inputformatoptions [dict get $args -inputformatoptions] + set opt_outputformat [dict get $args -outputformat] + set opt_outputformatoptions [dict get $args -outputformatoptions] + dict unset args -showsplits + dict unset args -splits + if {$debug} { + puts stdout "natsort::sort processed_args: $args" + if {$debug == 1} { + puts stdout "natsort::sort - try also -debug 2, -debug 3" + } + } + + #set sortmethod "-dictionary" ;# sorts a2b before a001b - possibly other strangenesses that are hard to reason about + + if {$sortmethod in [list dictionary ascii]} { + set sortmethod "-$sortmethod" + # -ascii is default for tcl lsort. + } else { + set sortmethod "-ascii" + } + + set allowed_collations [list nocase] + if {$opt_collate ne "\uFFFF"} { + if {$opt_collate ni $allowed_collations} { + error "natsort::sort unknown value for -collate option. Only acceptable value(s): $allowed_collations" + } + set nocaseopt "-$opt_collate" + } else { + set nocaseopt "" + } + set allowed_inputformats [list tcl raw csv words] + if {$opt_inputformat ni $allowed_inputformats} { + error "natsort::sort unknown value for -inputformat option. Only acceptable value(s): $allowed_inputformats" + } + set allowed_outputformats [list tcl raw csv words] + if {$opt_inputformat ni $allowed_outputformats} { + error "natsort::sort unknown value for -outputformat option. Only acceptable value(s): $allowed_outputformats" + } + + # + set winsplits [list / . _] + set commonsplits [list / . _ -] + #set commonsplits [list] + + set tagconfig [dict create] + dict set tagconfig last_part_text_tag "<19>" + if {$winlike} { + set splitchars $winsplits + #windows explorer sorts leading spaces at the top - which doesn't seem very helpful for keeping things together - but the explorer doesn't seem able to create leading spaces anyway. + set wintop [list "(" ")" { } {.} {_}] ;#windows specific order + foreach t $topchars { + if {$t ni $wintop} { + lappend wintop $t + } + } + set topchars $wintop + dict set tagconfig last_part_text_tag "" + } else { + set splitchars $commonsplits + } + if {$splits ne "\uFFFF"} { + set splitchars $splits + } + dict set tagconfig original_splitchars $splitchars + dict set tagconfig showsplits $showsplits + + #create topdict + set i 0 + set topdict [dict create] + foreach c $topchars { + incr i ;#start at 01 so that 00 reserved for final-split tag (allows x-0.1.txt to sort above x-0.1.1.txt by default. Use tagconfig to change, or choose -winlike 1 for explorer-like sorting) + dict set topdict $c "<0$i>" + } + set keylist [list] + + + if {$opt_inputformat eq "tcl"} { + set lineinput_transform [list _lineinput_as_tcl $opt_inputformatoptions] + } elseif {$opt_inputformat eq "csv"} { + set lineinput_transform [list _lineinput_as_csv $opt_inputformatoptions] + } elseif {$opt_inputformat eq "raw"} { + set lineinput_transform [list _lineinput_as_raw $opt_inputformatoptions] + } elseif {$opt_inputformat eq "words"} { + set lineinput_transform [list _lineinput_as_words $opt_inputformatoptions] + } + if {$opt_outputformat eq "tcl"} { + set lineoutput_transform [list _lineoutput_as_tcl $opt_outputformatoptions] + } elseif {$opt_outputformat eq "csv"} { + set lineoutput_transform [list _lineoutput_as_csv $opt_outputformatoptions] + } elseif {$opt_outputformat eq "raw"} { + set lineoutput_transform [list _lineoutput_as_raw $opt_outputformatoptions] + } elseif {$opt_outputformat eq "words"} { + set lineoutput_transform [list _lineoutput_as_words $opt_outputformatoptions] + } + + + if {("data" in $opt_inputformatapply) || ("index" in $opt_inputformatapply)} { + if {$opt_inputformat eq "raw"} { + set tf_stringlist $stringlist + } else { + set tf_stringlist [list] + foreach v $stringlist { + lappend tf_stringlist [{*}$lineinput_transform $v] + } + } + if {"data" in $opt_inputformatapply} { + set tf_data_stringlist $tf_stringlist + } else { + set tf_data_stringlist $stringlist + } + if {"index" in $opt_inputformatapply} { + set tf_index_stringlist $tf_stringlist + } else { + set tf_index_stringlist $stringlist + } + } else { + set tf_data_stringlist $stringlist + set tf_index_stringlist $stringlist + } + + + + if {$stacktrace} { + puts stdout [natsort::stacktrace] + set natsort::stacktrace_on 1 + } + if {$cols eq "\uFFFF"} { + set colkeys [lmap v $stringlist {}] + } else { + set colkeys [list] + foreach v $tf_index_stringlist { + set lineparts $v + set k [list] + foreach c $cols { + lappend k [lindex $lineparts $c] + } + lappend colkeys [join $k "_"] ;#use a common-split char - Review + } + } + #puts stdout "colkeys: $colkeys" + + if {$opt_inputformat eq "raw"} { + #no inputformat was applied - can just use stringlist + foreach value $stringlist ck $colkeys { + set contentindex [build_key $value $splitchars $topdict $tagconfig $debug] + set colindex [build_key $ck $splitchars $topdict $tagconfig $debug] + lappend keylist ${colindex}-${contentindex}-$value ;#note: entire raw value used for final sort disambiguation (can be whitespace that was ignored in indexing) + } + } else { + foreach keyinput $tf_index_stringlist datavalue $tf_data_stringlist ck $colkeys { + #data may or may not have been transformed + #column index may or may not have been built with transformed data + + set contentindex [build_key $keyinput $splitchars $topdict $tagconfig $debug] + set colindex [build_key $ck $splitchars $topdict $tagconfig $debug] + lappend keylist ${colindex}-${contentindex}-$datavalue ;#note: entire value used for final sort disambiguation (can be whitespace that was ignored in indexing) + } + } + #puts stderr "keylist: $keylist" + + ################################################################################################### + # Use the generated keylist to do the actual sorting + # select either the transformed or raw data as the corresponding output + ################################################################################################### + if {[string length $nocaseopt]} { + set sortcommand [list lsort $sortmethod $nocaseopt -indices $keylist] + } else { + set sortcommand [list lsort $sortmethod -indices $keylist] + } + if {$opt_outputformat eq "raw"} { + #raw output means no further transformations - it doesn't mean there wasn't a transform applied on the input side + #use the tf_data_stringlist in the output - which will be the same as the input stringlist if no input transform applied for data. + #(Also - it may or may not have been *sorted* on transformed data depending on whether 'index' was in $opt_inputformatapply) + foreach idx [{*}$sortcommand] { + lappend result [lindex $tf_data_stringlist $idx] + } + } else { + #we need to apply an output format + #The data may or may not have been transformed at input + foreach idx [{*}$sortcommand] { + lappend result [{*}$lineoutput_transform [lindex $tf_data_stringlist $idx]] + } + } + ################################################################################################### + + + + + + if {$debug >= 2} { + set screen_width 250 + set max_val 0 + set max_idx 0 + ##### calculate colum widths + foreach i [{*}$sortcommand] { + set len_val [string length [lindex $stringlist $i]] + if {$len_val > $max_val} { + set max_val $len_val + } + set len_idx [string length [lindex $keylist $i]] + if {$len_idx > $max_idx} { + set max_idx $len_idx + } + } + #### + set l_width [expr {$max_val + 1}] + set leftcol [string repeat " " $l_width] + set r_width [expr {$screen_width - $l_width - 1}] + set rightcol [string repeat " " $r_width] + set str [overtype::left $leftcol RAW] + puts stdout " $str Index with possibly transformed data at tail" + foreach i [{*}$sortcommand] { + #puts stdout "|d> [overtype::left $leftcol [lindex $stringlist $i] ] [lindex $keylist $i]" + set index [lindex $keylist $i] + set len_idx [string length $index] + set rowcount [expr {$len_idx / $r_width}] + if {($len_idx % $r_width) > 0} { + incr rowcount + } + set rows [list] + for {set r 0} {$r < $rowcount} {incr r} { + lappend rows [string range $index 0 $r_width-$r] + set index [string range $index $r_width end] + } + + set r 0 + foreach idxpart $rows { + if {$r == 0} { + #use the untransformed stringlist + set str [overtype::left $leftcol [lindex $stringlist $i]] + } else { + set str [overtype::left $leftcol ...]] + } + puts stdout " $str $idxpart" + incr r + } + #puts stdout "|> '[lindex $stringlist $i]'" + #puts stdout "|> [lindex $keylist $i]" + } + + puts stdout "|debug> topdict: $topdict" + puts stdout "|debug> splitchars: $splitchars" + } + return $result + } + + + + #Note that although leading whitespace isn't a commonly used feature of filesystem names - it's possible at least on FreeBSD,windows and linux so we should try to handle it sensibly. + proc sort_experiment {stringlist args} { + package require sqlite3 + + variable debug + set args [check_flags -caller natsort::sort \ + -defaults [dict create -db :memory: -collate nocase -nullvalue "->NULL<" -winlike 0 -topchars [list] -debug 0] \ + -extras {all} \ + -values $args] + set db [string trim [dict get $args -db]] + set collate [string trim [dict get $args -collate]] + set winlike [string trim [dict get $args -winlike]] + set debug [string trim [dict get $args -debug]] + set nullvalue [string trim [dict get $args -nullvalue]] + + + set topchars [string trim [dict get $args -topchars]] + + set topdot [expr {"." in $topchars}] + set topunderscore [expr {"_" in $topchars}] + + + sqlite3 db_natsort2 $db + #-- + #our table must handle the name with the greatest number of numeric/non-numeric splits. + #This means a single list member with pathological naming e.g a1a1a1a1a1a1a1a1a1a1a1.txt could greatly extend the number of columns and indices and affect performance. + #review: could be optimised to aggregate the tail into a single index, as the the extra columns won't assist in ordering, but make the table and query bigger. + # we should probably determine the longest common sequence of splits in the input list and add only one more index for the segment after that. + set maxsegments 0 + #-- + set prefix "idx" + + #note - there will be more columns in the sorting table than segments. + # (a segment equals one of the numeric & non-numeric string portions returned from 'split_numeric_sgements') + #--------------------------- + # consider + # a123b.v1.2.txt + # a123b.v1.3beta1.txt + # these have the following segments: + # a 123 b.v 1 . 2 .txt + # a 123 b.v 1 . 3 beta 1 .txt + #--------------------------- + # The first string has 7 segments (numbered 0 to 6) + # the second string has 9 segments + # + # for example when the data has any elements in a segment position that are numeric (e.g 0001 123) + # - then an index column with numeric equivalents will be created (e.g 0001 becomes 1), and any non-numeric values in that column will get mapped to a negative value (for special cases) or a high value such as NULL (with NULLS LAST sql support) + # + # when a segment + + #cycle through all strings - we cannot build tabledef as we go because the column type depends on whether all segments for a particular column are text vs int-equivalent. + array set segmentinfo {} + foreach nm $stringlist { + set segments [split_numeric_segments $nm] + if {![string length [string trim [lindex $segments 0]]]} { + if {[string is digit [string trim [lindex $segments 1]]]} { + #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though) + set segments [lrange $segments 1 end] + } + } + + + set c 0 ;#start of index columns + if {[llength $segments] > $maxsegments} { + set maxsegments [llength $segments] + } + foreach seg $segments { + set seg [string trim $seg] + set column_exists [info exists segmentinfo($c,type)] + if {[string is digit $seg]} { + if {$column_exists} { + #override it (may currently be text or int) + set segmentinfo($c,type) "int" + } else { + #new column + set segmentinfo($c,name) ${prefix}$c + set segmentinfo($c,type) "int" + } + } else { + #text never overrides int + if {!$column_exists} { + set segmentinfo($c,name) ${prefix}$c + set segmentinfo($c,type) "text" + } + } + incr c + } + } + if {$debug} { + puts stdout "Largest number of num/non-num segments in data: $maxsegments" + #parray segmentinfo + } + + # + set tabledef "" + set ordered_column_names [list] + set ordered_segmentinfo_tags [lsort -dictionary [array names segmentinfo *]] + foreach k $ordered_segmentinfo_tags { + lassign [split $k ,] c tag + if {$tag eq "type"} { + set type [set segmentinfo($k)] + if {$type eq "int"} { + append tabledef "$segmentinfo($c,name) int," + } else { + append tabledef "$segmentinfo($c,name) text COLLATE $collate," + } + append tabledef "raw$c text COLLATE $collate," + lappend ordered_column_names $segmentinfo($c,name) + lappend ordered_column_names raw$c ;#additional index column not in segmentinfo + } + if {$tag eq "name"} { + #lappend ordered_column_names $segmentinfo($k) + } + } + append tabledef "name text" + + #puts stdout "tabledef:$tabledef" + + + db_natsort2 eval [string map [list %tabledef% $tabledef] {create table natsort(%tabledef%)}] + + + foreach nm $stringlist { + array unset intdata + array set intdata {} + array set rawdata {} + #init array and build sql values string + set sql_insert "insert into natsort values(" + for {set i 0} {$i < $maxsegments} {incr i} { + set intdata($i) "" + set rawdata($i) "" + append sql_insert "\$intdata($i),\$rawdata($i)," + } + append sql_insert "\$nm" ;#don't manipulate name value in any way - e.g must leave all whitespace as the sort must return exactly the same elements as in the original list. + append sql_insert ")" + + set segments [split_numeric_segments $nm] + if {![string length [string trim [lindex $segments 0]]]} { + if {[string is digit [string trim [lindex $segments 1]]]} { + #name is whitespace followed by a digit - special case - ignore the whitespace for numbers only. (whitespace still goes through to name column though) + set segments [lrange $segments 1 end] + } + } + set values "" + set c 0 + foreach seg $segments { + if {[set segmentinfo($c,type)] eq "int"} { + if {[string is digit [string trim $seg]]} { + set intdata($c) [trimzero [string trim $seg]] + } else { + catch {unset intdata($c)} ;#set NULL - sorts last + if {($c == 0) && ($topunderscore) && [string match _* [string trim $seg]]} { + set intdata($c) -100 + } + if {($c == 0) && ($topdot) && [string match .* [string trim $seg]]} { + set intdata($c) -50 + } + } + set rawdata($c) [string trim $seg] + } else { + #pure text column + #set intdata($c) [string trim $seg] ;#ignore leading/trailing whitespace - we sort first on trimmed version, then refine with the sort on rawdata index + #catch {unset indata($c)} + set indata($c) [string trim $seg] + set rawdata($c) $seg + } + #set rawdata($c) [string trim $seg]# + #set rawdata($c) $seg + incr c + } + db_natsort2 eval $sql_insert + } + + set orderedlist [list] + + if {$debug} { + db_natsort2 eval {select * from pragma_table_info('natsort')} rowdata { + parray rowdata + } + } + set orderby "order by " + + foreach cname $ordered_column_names { + if {[string match "idx*" $cname]} { + append orderby "$cname ASC NULLS LAST," + } else { + append orderby "$cname ASC," + } + } + append orderby " name ASC" + #append orderby " NULLS LAST" ;#?? + + #e.g "order by idx0 ASC, raw0 ASC, idx1 ASC .... name ASC" + if {$debug} { + puts stdout "orderby clause: $orderby" + } + db_natsort2 eval [string map [list %orderby% $orderby] {select * from natsort %orderby%}] rowdata { + set line "- " + #parray rowdata + set columnnames $rowdata(*) + #puts stdout "columnnames: $columnnames" + #[lsort -dictionary [array names rowdata] + append line "$rowdata(name) \n" + foreach nm $columnnames { + if {$nm ne "name"} { + append line "$nm: $rowdata($nm) " + } + } + #puts stdout $line + #puts stdout "$rowdata(name)" + lappend orderedlist $rowdata(name) + } + + db_natsort2 close + return $orderedlist + } +} + + +#application section e.g this file might be linked from /usr/local/bin/natsort +namespace eval natsort { + namespace import ::flagfilter::check_flags + + proc called_directly_namematch {} { + global argv0 + #see https://wiki.tcl-lang.org/page/main+script + #trailing ... let's us resolve symlinks in last component of the path (could be something else like ___ but ... seems unlikely to collide with anything in the filesystem) + if {[info exists argv0] + && + [file dirname [file normalize [file join [info script] ...]]] + eq + [file dirname [file normalize [file join $argv0 ...]]] + } { + return 1 + } else { + #puts stdout "norm info script: [file dirname [file normalize [file join [info script] ...]]]" + #puts stdout "norm argv0 : [file dirname [file normalize [file join $argv0 ...]]]" + return 0 + } + } + #Review issues around comparing names vs using inodes (esp with respect to samba shares) + proc called_directly_inodematch {} { + global argv0 + if {[info exists argv0] + && [file exists [info script]] && [file exists $argv0]} { + file stat $argv0 argv0Info + file stat [info script] scriptInfo + expr {$argv0Info(dev) == $scriptInfo(dev) + && $argv0Info(ino) == $scriptInfo(ino)} + } else { + return 0 + } + } + + set is_namematch [called_directly_namematch] + set is_inodematch [called_directly_inodematch] + #### + #review - reliability of mechanisms to determine direct calls + # we don't want application being called when being used as a library, but we need it to run if called directly or from symlinks etc + #-- choose a policy and leave the others commented. + #set is_called_directly $is_namematch + #set is_called_directly $is_inodematch + set is_called_directly [expr {$is_namematch || $is_inodematch}] + #set is_called_directly [expr {$is_namematch && $is_inodematch}] + ### + + + #puts stdout "called_directly_name: [called_directly_namematch] called_directly_inode: [called_directly_inodematch]" + + + # + + + proc test_pass_fail_message {pass {additional ""}} { + variable test_fail_msg + variable test_pass_msg + if {$pass} { + puts stderr $test_pass_msg + } else { + puts stderr $test_fail_msg + } + puts stderr $additional + } + + variable test_fail_msg "XXXXXXXXXXXX FAIL XXXXXXXXXXXXX" + variable test_pass_msg "------------ PASS -------------" + proc test_sort_1 {args} { + package require struct::list + puts stderr "---$args" + set args [check_flags -caller natsort:test_sort_1 -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] -values $args] + + puts stderr "test_sort_1 got args: $args" + + set unsorted_input { + 2.2.2 + 2.2.2.2 + 1a.1.1 + 1a.2.1.1 + 1.12.1 + 1.2.1.1 + 1.02.1.1 + 1.002b.1.1 + 1.1.1.2 + 1.1.1.1 + } + set input { +1.1.1 +1.1.1.2 +1.002b.1.1 +1.02.1.1 +1.2.1.1 +1.12.1 +1a.1.1 +1a.2.1.1 +2.2.2 +2.2.2.2 + } + + set sorted [natsort::sort $input {*}$args] + set is_match [struct::list equal $input $sorted] + + set msg "windows-explorer order" + + test_pass_fail_message $is_match $msg + puts stdout [string repeat - 40] + puts stdout INPUT + puts stdout [string repeat - 40] + foreach item $input { + puts stdout $item + } + puts stdout [string repeat - 40] + puts stdout OUTPUT + puts stdout [string repeat - 40] + foreach item $sorted { + puts stdout $item + } + test_pass_fail_message $is_match $msg + return [expr {!$is_match}] + } + proc test_sort_showsplits {args} { + package require struct::list + + set args [check_flags -caller natsort:test_sort_1 \ + -defaults [list -collate nocase -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 1 ] \ + -extras {all} \ + -values $args] + + set input1 { + a-b.txt + a.b.c.txt + b.c-txt + } + + + set input2 { + a.b.c.txt + a-b.txt + b.c-text + } + + foreach {msg testlist } [list "custom-order" $input1 "windows-explorer (should work with -winlike 1)" $input2] { + set sorted [natsort::sort $testlist {*}$args] + set is_match [struct::list equal $testlist $sorted] + + test_pass_fail_message $is_match $msg + puts stderr "INPUT" + puts stderr "[string repeat - 40]" + foreach item $testlist { + puts stdout $item + } + puts stderr "[string repeat - 40]" + puts stderr "OUTPUT" + puts stderr "[string repeat - 40]" + foreach item $sorted { + puts stdout $item + } + + test_pass_fail_message $is_match $msg + } + + #return [expr {!$is_match}] + + } + + #tcl dispatch order - non flag items up front + #trailing flags are paired even if supplied as solo flags e.g -l becomes -l 1 + proc commandline_ls {args} { + set operands [list] + set posn 0 + foreach a $args { + if {![string match -* $a]} { + lappend operands $a + } else { + set flag1_posn $posn + break + } + incr posn + } + set args [lrange $args $flag1_posn end] + + + set debug 0 + set posn [lsearch $args -debug] + if {$posn > 0} { + if {[lindex $args $posn+1]} { + set debug [lindex $args $posn+1] + } + } + if {$debug} { + puts stderr "|debug>commandline_ls got $args" + } + + #if first operand not supplied - replace it with current working dir + if {[lindex $operands 0] eq "\uFFFF"} { + lset operands 0 [pwd] + } + + set targets [list] + foreach op $operands { + if {$op ne "\uFFFF"} { + set opchars [split [file tail $op] ""] + if {"?" in $opchars || "*" in $opchars} { + lappend targets $op + } else { + #actual file or dir + set targetitem $op + set targetitem [file normalize $op] + if {![file exists $targetitem]} { + if {$debug} { + puts stderr "|debug>commandline_ls Unable to access path '$targetitem'" + } + } + lappend targets $targetitem + if {$debug} { + puts stderr "|debug>commandline_ls listing for $targetitem" + } + } + } + } + set args [check_flags -caller commandline_ls \ + -return flagged|defaults \ + -debugargs 0 \ + -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 -algorithm sort] \ + -required {all} \ + -extras {all} \ + -soloflags {-v -l} \ + -commandprocessors {} \ + -values $args ] + if {$debug} { + puts stderr "|debug>args: $args" + } + + + set algorithm [dict get $args -algorithm] + dict unset args -algorithm + + set allfolders [list] + set allfiles [list] + foreach item $targets { + if {[file exists $item]} { + if {[file type $item] eq "directory"} { + set dotfolders [glob -nocomplain -directory $item -type {d} -tail .*] + set folders [glob -nocomplain -directory $item -type {d} -tail *] + set allfolders [concat $allfolders $dotfolders $folders] + + set dotfiles [glob -nocomplain -directory $item -type {f} -tail .*] + set files [glob -nocomplain -directory $item -type {f} -tail *] + set allfiles [concat $allfiles $dotfiles $files] + } else { + #file (or link?) + set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]] + set allfiles [concat $allfiles $files] + } + } else { + set folders [glob -nocomplain -directory $item -type {d} -tail [file tail $item]] + set allfolders [concat $allfolders $folders] + set files [glob -nocomplain -directory [file dirname $item] -tail [file tail $item]] + set allfiles [concat $allfiles $files] + } + } + + + set sorted_folders [natsort::sort $allfolders {*}$args] + set sorted_files [natsort::sort $allfiles {*}$args] + + foreach fold $sorted_folders { + puts stdout $fold + } + foreach file $sorted_files { + puts stdout $file + } + + return "-- ok printed to stdout [llength $sorted_folders] folders and [llength $sorted_files] files --" + } + + package require argp + argp::registerArgs commandline_test { + { -showsplits boolean 0} + { -stacktrace boolean 0} + { -debug boolean 0} + { -winlike boolean 0} + { -db string ":memory:"} + { -collate string "nocase"} + { -algorithm string "sort"} + { -topchars string "\uFFFF"} + { -testlist string {10 1 30 3}} + } + argp::setArgsNeeded commandline_test {-stacktrace} + proc commandline_test {test args} { + variable testlist + puts stdout "commandline_test got $args" + argp::parseArgs opts + puts stdout "commandline_test got [array get opts]" + set args [check_flags -caller natsort_commandline \ + -return flagged|defaults \ + -defaults [list -db :memory: -collate nocase -testlist $testlist -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \ + -values $args] + + if {[string tolower $test] in [list "1" "true"]} { + set test "sort" + } else { + if {![llength [info commands $test]]} { + error "test $test not found" + } + } + dict unset args -test + set stacktrace [dict get $args -stacktrace] + # dict unset args -stacktrace + + set argtestlist [dict get $args -testlist] + dict unset args -testlist + + + set debug [dict get $args -debug] + + set collate [dict get $args -collate] + set db [dict get $args -db] + set winlike [dict get $args -winlike] + set topchars [dict get $args -topchars] + + + puts stderr "|test>-----start natsort::$test--- input list size : [llength $argtestlist]" + #set resultlist [$test $argtestlist -db $db -collate $collate -topchars $topchars -winlike $winlike] + set resultlist [$test $argtestlist {*}$args] + foreach nm $resultlist { + puts stdout $nm + } + puts stdout "|test>-----end natsort::$test--- sorted list size: [llength $resultlist]" + return "test end" + } + proc commandline_runtests {runtests args} { + set argvals [check_flags -caller commandline_runtests \ + -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits "\uFFFF" -runtests 1] \ + -values $args] + + puts stderr "runtests args: $argvals" + + #set runtests [dict get $argvals -runtests] + dict unset argvals -runtests + dict unset argvals -algorithm + + puts stderr "runtests args: $argvals" + #exit 0 + + set test_prefix "::natsort::test_sort_" + + if {$runtests eq "1"} { + set runtests "*" + } + + + set testcommands [info commands ${test_prefix}${runtests}] + if {![llength $testcommands]} { + puts stderr "No test commands matched -runtests argument '$runtests'" + puts stderr "Use 1 to run all tests" + set alltests [info commands ${test_prefix}*] + puts stderr "Valid tests are:" + + set prefixlen [string length $test_prefix] + foreach t $alltests { + set shortname [string range $t $prefixlen end] + puts stderr "$t = -runtests $shortname" + } + + } else { + foreach cmd $testcommands { + puts stderr [string repeat - 40] + puts stderr "calling $cmd with args: '$argvals'" + puts stderr [string repeat - 40] + $cmd {*}$argvals + } + } + exit 0 + } + proc help {args} { + puts stdout "natsort::help got '$args'" + return "Help not implemented" + } + proc natsort_pipe {args} { + #PIPELINE to take input list on stdin and write sorted list to stdout + #strip - from arglist + #set args [check_flags -caller natsort_pipeline \ + # -return all \ + # -defaults [list -db :memory: -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \ + # -values $args] + + + set debug [dict get $args -debug] + if {$debug} { + puts stderr "|debug> natsort_pipe got args:'$args'" + } + set algorithm [dict get $args -algorithm] + dict unset args -algorithm + + set proclist [info commands ::natsort::sort*] + set algos [list] + foreach p $proclist { + lappend algos [namespace tail $p] + } + if {$algorithm ni [list {*}$proclist {*}$algos]} { + do_error "valid sort mechanisms: $algos" 2 + } + + + set input_list [list] + while {![eof stdin]} { + if {[gets stdin line] > 0} { + lappend input_list $line + } else { + if {[eof stdin]} { + + } else { + after 10 + } + } + } + + if {$debug} { + puts stderr "|debug> received [llength $input_list] list elements" + } + + set resultlist [$algorithm $input_list {*}$args] + if {$debug} { + puts stderr "|debug> returning [llength $resultlist] list elements" + } + foreach r $resultlist { + puts stdout $r + } + #exit 0 + + } + if {($is_called_directly)} { + set cmdprocessors { + {helpfinal {match "^help$" dispatch natsort::help}} + {helpfinal {sub -topic default "NONE"}} + } + #set args [check_flags \ + # -caller test1 \ + # -debugargs 2 \ + # -return arglist \ + # -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] \ + # -required {none} \ + # -extras {all} \ + # -commandprocessors $cmdprocessors \ + # -values $::argv ] + interp alias {} do_filter {} ::flagfilter::check_flags + + #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld + set cmdprocessors { + {helpcmd {match "^help$" dispatch natsort::help singleopts {-v}}} + {helpcmd {sub -operand default \uFFFF singleopts {-l}}} + {lscmd {match "^ls$" dispatch natsort::commandline_ls dispatchtype tcl dispatchglobal 1 mashopts {-l -a} singleopts {-l -a} pairopts {} longopts {--color=always}}} + {lscmd {sub dir default "\uFFFF"}} + {lscmd {sub dir2 default "\uFFFF"}} + {lscmd {sub dir3 default "\uFFFF"}} + {lscmd {sub dir4 default "\uFFFF"}} + {lscmd {sub dir5 default "\uFFFF"}} + {lscmd {sub dir6 default "\uFFFF"}} + {runtests {match "^-tests$" dispatch natsort::commandline_runtests singleopts {-l}}} + {runtests {sub testname default "1" singleopts {-l}}} + {pipecmd {match "^-$" dispatch natsort::natsort_pipe dispatchtype tcl}} + } + set arglist [do_filter \ + -debugargs 0 \ + -debugargsonerror 2 \ + -caller cline_dispatch1 \ + -return all \ + -soloflags {-v -x} \ + -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] \ + -required {all} \ + -extras {all} \ + -commandprocessors $cmdprocessors \ + -values $::argv ] + + + #mashopts are generally single-letter opts that can be run together e.g -l -d as -ld + set cmdprocessors { + {testcmd {match "^test$" dispatch natsort::commandline_test singleopts {-l}}} + {testcmd {sub testname default "1" singleopts {-l}}} + } + set arglist [check_flags \ + -debugargs 0 \ + -caller cline_dispatch2 \ + -return all \ + -soloflags {-v -l} \ + -defaults [list -collate nocase -algorithm sort -testlist "1 2 3 10" -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0 ] \ + -required {all} \ + -extras {all} \ + -commandprocessors $cmdprocessors \ + -values $::argv ] + + + + + #set cmdprocessors [list] + #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors $cmdprocessors -values $::argv ] + + #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {-cmd {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ] + #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {{-cmd -default help} {-cmd -cmdarg1 -default "."} {-cmd -cmdarg2 -default j}} -values $::argv ] + #set args [check_flags -caller test1 -defaults [list -collate nocase -algorithm sort -topchars "\uFFFF" -winlike 0 -debug 0 -stacktrace 0 -showsplits 0] -required {all} -extras {none} -commandprocessors {ls {ls lsdir -default "\uFFFF"}} -values $::argv ] + + exit 0 + + if {$::argc} { + + } + } +} + + + + diff --git a/src/vendormodules/oolib-0.1.tm b/src/vendormodules/oolib-0.1.tm new file mode 100644 index 00000000..9cf1ca07 --- /dev/null +++ b/src/vendormodules/oolib-0.1.tm @@ -0,0 +1,195 @@ +#JMN - api should be kept in sync with package patternlib where possible +# +package provide oolib [namespace eval oolib { + variable version + set version 0.1 +}] + +namespace eval oolib { + oo::class create collection { + variable o_data ;#dict + variable o_alias + constructor {} { + set o_data [dict create] + } + method info {} { + return [dict info $o_data] + } + method count {} { + return [dict size $o_data] + } + method isEmpty {} { + expr {[dict size $o_data] == 0} + } + method names {{globOrIdx {}}} { + if {[llength $globOrIdx]} { + if {[string is integer -strict $globOrIdx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $idx} result]} { + error "[self object] no such index : '$idx'" + } else { + return $result + } + } else { + #glob + return [lsearch -glob -all -inline [dict keys $o_data] $globOrIdx] + } + } else { + return [dict keys $o_data] + } + } + #like names but without globbing + method keys {} { + dict keys $o_data + } + method key {{posn 0}} { + if {$posn < 0} { + set posn "end-[expr {abs($posn + 1)}]" + } + if {[catch {lindex [dict keys $o_data] $posn} result]} { + error "[self object] no such index : '$posn'" + } else { + return $result + } + } + method hasKey {key} { + dict exists $o_data $key + } + method get {} { + return $o_data + } + method items {} { + return [dict values $o_data] + } + method item {key} { + if {[string is integer -strict $key]} { + if {$key > 0} { + set valposn [expr {(2*$key) +1}] + return [lindex $o_data $valposn] + } else { + set key "end-[expr {abs($key + 1)}]" + return [lindex [dict keys $o_data] $key] + } + } + if {[dict exists $o_data $key]} { + return [dict get $o_data $key] + } + } + #inverse lookup + method itemKeys {value} { + set value_indices [lsearch -all [dict values $o_data] $value] + set keylist [list] + foreach i $value_indices { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + method search {value args} { + set matches [lsearch {*}$args [dict values $o_data] $value] + if {"-inline" in $args} { + return $matches + } else { + set keylist [list] + foreach i $matches { + set idx [expr {(($i + 1) *2) -2}] + lappend keylist [lindex $o_data $idx] + } + return $keylist + } + } + #review - see patternlib. Is the intention for aliases to be configurable independent of whether the target exists? + method alias {newAlias existingKeyOrAlias} { + if {[string is integer -strict $newAlias]} { + error "[self object] collection key alias cannot be integer" + } + if {[string length $existingKeyOrAlias]} { + set o_alias($newAlias) $existingKeyOrAlias + } else { + unset o_alias($newAlias) + } + } + method aliases {{key ""}} { + if {[string length $key]} { + set result [list] + foreach {n v} [array get o_alias] { + if {$v eq $key} { + lappend result $n $v + } + } + return $result + } else { + return [array get o_alias] + } + } + #if the supplied index is an alias, return the underlying key; else return the index supplied. + method realKey {idx} { + if {[catch {set o_alias($idx)} key]} { + return $idx + } else { + return $key + } + } + method add {value key} { + if {[string is integer -strict $key]} { + error "[self object] collection key must not be an integer. Use another structure if integer keys required" + } + if {[dict exists $o_data $key]} { + error "[self object] col_processors object error: key '$key' already exists in collection" + } + dict set o_data $key $value + return [expr {[dict size $o_data] - 1}] ;#return index of item + } + method remove {idx {endRange ""}} { + if {[string length $endRange]} { + error "[self object] collection error: ranged removal not yet implemented.. remove one item at a time" + } + if {[string is integer -strict $idx]} { + if {$idx < 0} { + set idx "end-[expr {abs($idx+1)}]" + } + set key [lindex [dict keys $o_data] $idx] + set posn $idx + } else { + set key $idx + set posn [lsearch -exact [dict keys $o_data] $key] + if {$posn < 0} { + error "[self object] no such index: '$idx' in this collection" + } + } + dict unset o_data $key + return + } + method clear {} { + set o_data [dict create] + return + } + method reverse {} { + set dictnew [dict create] + foreach k [lreverse [dict keys $o_data]] { + dict set dictnew $k [dict get $o_data $k] + } + set o_data $dictnew + return + } + #review - cmd as list vs cmd as script? + method map {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list {*}$cmd $v]] + } + return $seed + } + method objectmap {cmd} { + set seed [list] + dict for {k v} $o_data { + lappend seed [uplevel #0 [list $v {*}$cmd]] + } + return $seed + } + } + +} + diff --git a/src/deps/pattern-1.2.4.tm b/src/vendormodules/pattern-1.2.4.tm similarity index 100% rename from src/deps/pattern-1.2.4.tm rename to src/vendormodules/pattern-1.2.4.tm diff --git a/src/deps/patterncmd-1.2.4.tm b/src/vendormodules/patterncmd-1.2.4.tm similarity index 100% rename from src/deps/patterncmd-1.2.4.tm rename to src/vendormodules/patterncmd-1.2.4.tm diff --git a/src/deps/patternlib-1.2.6.tm b/src/vendormodules/patternlib-1.2.6.tm similarity index 100% rename from src/deps/patternlib-1.2.6.tm rename to src/vendormodules/patternlib-1.2.6.tm diff --git a/src/deps/patternpredator2-1.2.4.tm b/src/vendormodules/patternpredator2-1.2.4.tm similarity index 100% rename from src/deps/patternpredator2-1.2.4.tm rename to src/vendormodules/patternpredator2-1.2.4.tm