Browse Source

dir listing fixes, du, pmix visible_lib_copy_to_modulefolder

master
Julian Noble 1 year ago
parent
commit
a9094a8716
  1. 24
      src/bootsupport/README.md
  2. 200
      src/bootsupport/cksum-1.1.4.tm
  3. 2311
      src/bootsupport/fileutil-1.16.1.tm
  4. 207
      src/bootsupport/fileutil/decode-0.2.1.tm
  5. 28
      src/bootsupport/fileutil/multi-0.1.tm
  6. 645
      src/bootsupport/fileutil/multi/op-0.5.3.tm
  7. 74
      src/bootsupport/fileutil/paths-1.tm
  8. 504
      src/bootsupport/fileutil/traverse-0.6.tm
  9. 789
      src/bootsupport/punk/du-0.1.0.tm
  10. 1375
      src/bootsupport/punk/mix-0.2.tm
  11. 814
      src/bootsupport/punk/repo-0.1.0.tm
  12. 321
      src/bootsupport/punk/winpath-0.1.0.tm
  13. 79
      src/make.tcl
  14. 325
      src/modules/punk-0.1.tm
  15. 618
      src/modules/punk/du-999999.0a1.0.tm
  16. 196
      src/modules/punk/mix-0.2.tm
  17. 24
      src/modules/punk/mix/templates/layouts/project/src/bootsupport/README.md
  18. 21
      src/modules/punk/repl-0.1.tm
  19. 119
      src/modules/punk/repo-999999.0a1.0.tm
  20. 44
      src/modules/punk/winpath-999999.0a1.0.tm

24
src/bootsupport/README.md

@ -0,0 +1,24 @@
This is primarily for tcl .tm modules required for your bootstrapping/make/build process.
It could include other files necessary for this process.
The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries.
The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src.
The modules can be your own, or 3rd party such as individual items from tcllib.
You can copy modules from a running punk shell to this location using the pmix command.
e.g
>pmix visible_lib_copy_to_modulefolder some::module::lib bootsupport
The pmix command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package.
e.g the result might be a file such as
<projectname>/src/bootsupport/some/module/lib-0.1.tm
The originating library may not yet be in .tm form.
You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only.
Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works.

200
src/bootsupport/cksum-1.1.4.tm

@ -0,0 +1,200 @@
# cksum.tcl - Copyright (C) 2002 Pat Thoyts <patthoyts@users.sourceforge.net>
#
# Provides a Tcl only implementation of the unix cksum(1) command. This is
# similar to the sum(1) command but the algorithm is better defined and
# standardized across multiple platforms by POSIX 1003.2/D11.2
#
# This command has been verified against the cksum command from the GNU
# textutils package version 2.0
#
# -------------------------------------------------------------------------
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# -------------------------------------------------------------------------
package require Tcl 8.2; # tcl minimum version
namespace eval ::crc {
namespace export cksum
variable cksum_tbl [list 0x0 \
0x04C11DB7 0x09823B6E 0x0D4326D9 0x130476DC 0x17C56B6B \
0x1A864DB2 0x1E475005 0x2608EDB8 0x22C9F00F 0x2F8AD6D6 \
0x2B4BCB61 0x350C9B64 0x31CD86D3 0x3C8EA00A 0x384FBDBD \
0x4C11DB70 0x48D0C6C7 0x4593E01E 0x4152FDA9 0x5F15ADAC \
0x5BD4B01B 0x569796C2 0x52568B75 0x6A1936C8 0x6ED82B7F \
0x639B0DA6 0x675A1011 0x791D4014 0x7DDC5DA3 0x709F7B7A \
0x745E66CD 0x9823B6E0 0x9CE2AB57 0x91A18D8E 0x95609039 \
0x8B27C03C 0x8FE6DD8B 0x82A5FB52 0x8664E6E5 0xBE2B5B58 \
0xBAEA46EF 0xB7A96036 0xB3687D81 0xAD2F2D84 0xA9EE3033 \
0xA4AD16EA 0xA06C0B5D 0xD4326D90 0xD0F37027 0xDDB056FE \
0xD9714B49 0xC7361B4C 0xC3F706FB 0xCEB42022 0xCA753D95 \
0xF23A8028 0xF6FB9D9F 0xFBB8BB46 0xFF79A6F1 0xE13EF6F4 \
0xE5FFEB43 0xE8BCCD9A 0xEC7DD02D 0x34867077 0x30476DC0 \
0x3D044B19 0x39C556AE 0x278206AB 0x23431B1C 0x2E003DC5 \
0x2AC12072 0x128E9DCF 0x164F8078 0x1B0CA6A1 0x1FCDBB16 \
0x018AEB13 0x054BF6A4 0x0808D07D 0x0CC9CDCA 0x7897AB07 \
0x7C56B6B0 0x71159069 0x75D48DDE 0x6B93DDDB 0x6F52C06C \
0x6211E6B5 0x66D0FB02 0x5E9F46BF 0x5A5E5B08 0x571D7DD1 \
0x53DC6066 0x4D9B3063 0x495A2DD4 0x44190B0D 0x40D816BA \
0xACA5C697 0xA864DB20 0xA527FDF9 0xA1E6E04E 0xBFA1B04B \
0xBB60ADFC 0xB6238B25 0xB2E29692 0x8AAD2B2F 0x8E6C3698 \
0x832F1041 0x87EE0DF6 0x99A95DF3 0x9D684044 0x902B669D \
0x94EA7B2A 0xE0B41DE7 0xE4750050 0xE9362689 0xEDF73B3E \
0xF3B06B3B 0xF771768C 0xFA325055 0xFEF34DE2 0xC6BCF05F \
0xC27DEDE8 0xCF3ECB31 0xCBFFD686 0xD5B88683 0xD1799B34 \
0xDC3ABDED 0xD8FBA05A 0x690CE0EE 0x6DCDFD59 0x608EDB80 \
0x644FC637 0x7A089632 0x7EC98B85 0x738AAD5C 0x774BB0EB \
0x4F040D56 0x4BC510E1 0x46863638 0x42472B8F 0x5C007B8A \
0x58C1663D 0x558240E4 0x51435D53 0x251D3B9E 0x21DC2629 \
0x2C9F00F0 0x285E1D47 0x36194D42 0x32D850F5 0x3F9B762C \
0x3B5A6B9B 0x0315D626 0x07D4CB91 0x0A97ED48 0x0E56F0FF \
0x1011A0FA 0x14D0BD4D 0x19939B94 0x1D528623 0xF12F560E \
0xF5EE4BB9 0xF8AD6D60 0xFC6C70D7 0xE22B20D2 0xE6EA3D65 \
0xEBA91BBC 0xEF68060B 0xD727BBB6 0xD3E6A601 0xDEA580D8 \
0xDA649D6F 0xC423CD6A 0xC0E2D0DD 0xCDA1F604 0xC960EBB3 \
0xBD3E8D7E 0xB9FF90C9 0xB4BCB610 0xB07DABA7 0xAE3AFBA2 \
0xAAFBE615 0xA7B8C0CC 0xA379DD7B 0x9B3660C6 0x9FF77D71 \
0x92B45BA8 0x9675461F 0x8832161A 0x8CF30BAD 0x81B02D74 \
0x857130C3 0x5D8A9099 0x594B8D2E 0x5408ABF7 0x50C9B640 \
0x4E8EE645 0x4A4FFBF2 0x470CDD2B 0x43CDC09C 0x7B827D21 \
0x7F436096 0x7200464F 0x76C15BF8 0x68860BFD 0x6C47164A \
0x61043093 0x65C52D24 0x119B4BE9 0x155A565E 0x18197087 \
0x1CD86D30 0x029F3D35 0x065E2082 0x0B1D065B 0x0FDC1BEC \
0x3793A651 0x3352BBE6 0x3E119D3F 0x3AD08088 0x2497D08D \
0x2056CD3A 0x2D15EBE3 0x29D4F654 0xC5A92679 0xC1683BCE \
0xCC2B1D17 0xC8EA00A0 0xD6AD50A5 0xD26C4D12 0xDF2F6BCB \
0xDBEE767C 0xE3A1CBC1 0xE760D676 0xEA23F0AF 0xEEE2ED18 \
0xF0A5BD1D 0xF464A0AA 0xF9278673 0xFDE69BC4 0x89B8FD09 \
0x8D79E0BE 0x803AC667 0x84FBDBD0 0x9ABC8BD5 0x9E7D9662 \
0x933EB0BB 0x97FFAD0C 0xAFB010B1 0xAB710D06 0xA6322BDF \
0xA2F33668 0xBCB4666D 0xB8757BDA 0xB5365D03 0xB1F740B4 ]
variable uid
if {![info exists uid]} {set uid 0}
}
# crc::CksumInit --
#
# Create and initialize a cksum context. This is cleaned up when we
# call CksumFinal to obtain the result.
#
proc ::crc::CksumInit {} {
variable uid
set token [namespace current]::[incr uid]
upvar #0 $token state
array set state {t 0 l 0}
return $token
}
proc ::crc::CksumUpdate {token data} {
variable cksum_tbl
upvar #0 $token state
set t $state(t)
binary scan $data c* r
foreach {n} $r {
set index [expr { (($t >> 24) ^ ($n & 0xFF)) & 0xFF }]
# Since the introduction of built-in bigInt support with Tcl
# 8.5, bit-shifting $t to the left no longer overflows,
# keeping it 32 bits long. The value grows bigger and bigger
# instead - a severe hit on performance. For this reason we
# do a bitwise AND against 0xFFFFFFFF at each step to keep the
# value within limits.
set t [expr {0xFFFFFFFF & (($t << 8) ^ [lindex $cksum_tbl $index])}]
incr state(l)
}
set state(t) $t
return
}
proc ::crc::CksumFinal {token} {
variable cksum_tbl
upvar #0 $token state
set t $state(t)
for {set i $state(l)} {$i > 0} {set i [expr {$i>>8}]} {
set index [expr {(($t >> 24) ^ $i) & 0xFF}]
set t [expr {0xFFFFFFFF & (($t << 8) ^ [lindex $cksum_tbl $index])}]
}
unset state
return [expr {~$t & 0xFFFFFFFF}]
}
# crc::Pop --
#
# Pop the nth element off a list. Used in options processing.
#
proc ::crc::Pop {varname {nth 0}} {
upvar $varname args
set r [lindex $args $nth]
set args [lreplace $args $nth $nth]
return $r
}
# Description:
# Provide a Tcl equivalent of the unix cksum(1) command.
# Options:
# -filename name - return a checksum for the specified file.
# -format string - return the checksum using this format string.
# -chunksize size - set the chunking read size
#
proc ::crc::cksum {args} {
array set opts [list -filename {} -channel {} -chunksize 4096 \
-format %u -command {}]
while {[string match -* [set option [lindex $args 0]]]} {
switch -glob -- $option {
-file* { set opts(-filename) [Pop args 1] }
-chan* { set opts(-channel) [Pop args 1] }
-chunk* { set opts(-chunksize) [Pop args 1] }
-for* { set opts(-format) [Pop args 1] }
-command { set opts(-command) [Pop args 1] }
default {
if {[llength $args] == 1} { break }
if {[string compare $option "--"] == 0} { Pop args ; break }
set err [join [lsort [array names opts -*]] ", "]
return -code error "bad option \"option\": must be $err"
}
}
Pop args
}
if {$opts(-filename) != {}} {
set opts(-channel) [open $opts(-filename) r]
fconfigure $opts(-channel) -translation binary
}
if {$opts(-channel) == {}} {
if {[llength $args] != 1} {
return -code error "wrong # args: should be\
cksum ?-format string?\
-channel chan | -filename file | string"
}
set tok [CksumInit]
CksumUpdate $tok [lindex $args 0]
set r [CksumFinal $tok]
} else {
set tok [CksumInit]
while {![eof $opts(-channel)]} {
CksumUpdate $tok [read $opts(-channel) $opts(-chunksize)]
}
set r [CksumFinal $tok]
if {$opts(-filename) != {}} {
close $opts(-channel)
}
}
return [format $opts(-format) $r]
}
# -------------------------------------------------------------------------
package provide cksum 1.1.4
# -------------------------------------------------------------------------
# Local variables:
# mode: tcl
# indent-tabs-mode: nil
# End:

2311
src/bootsupport/fileutil-1.16.1.tm

File diff suppressed because it is too large Load Diff

207
src/bootsupport/fileutil/decode-0.2.1.tm

@ -0,0 +1,207 @@
# -*- tcl -*-
# ### ### ### ######### ######### #########
## Copyright (c) 2008-2009 ActiveState Software Inc., Andreas Kupries
## 2016 Andreas Kupries
## BSD License
##
# Package to help the writing of file decoders. Provides generic
# low-level support commands.
package require Tcl 8.4
namespace eval ::fileutil::decode {
namespace export mark go rewind at
namespace export byte short-le long-le nbytes skip
namespace export unsigned match recode getval
namespace export clear get put putloc setbuf
}
# ### ### ### ######### ######### #########
##
proc ::fileutil::decode::open {fname} {
variable chan
set chan [::open $fname r]
fconfigure $chan \
-translation binary \
-encoding binary \
-eofchar {}
return
}
proc ::fileutil::decode::close {} {
variable chan
::close $chan
}
# ### ### ### ######### ######### #########
##
proc ::fileutil::decode::mark {} {
variable chan
variable mark
set mark [tell $chan]
return
}
proc ::fileutil::decode::go {to} {
variable chan
seek $chan $to start
return
}
proc ::fileutil::decode::rewind {} {
variable chan
variable mark
if {$mark == {}} {
return -code error \
-errorcode {FILE DECODE NO MARK} \
"No mark to rewind to"
}
seek $chan $mark start
set mark {}
return
}
proc ::fileutil::decode::at {} {
variable chan
return [tell $chan]
}
# ### ### ### ######### ######### #########
##
proc ::fileutil::decode::byte {} {
variable chan
variable mask 0xff
variable val [read $chan 1]
binary scan $val c val
return
}
proc ::fileutil::decode::short-le {} {
variable chan
variable mask 0xffff
variable val [read $chan 2]
binary scan $val s val
return
}
proc ::fileutil::decode::long-le {} {
variable chan
variable mask 0xffffffff
variable val [read $chan 4]
binary scan $val i val
return
}
proc ::fileutil::decode::nbytes {n} {
variable chan
variable mask {}
variable val [read $chan $n]
return
}
proc ::fileutil::decode::skip {n} {
variable chan
#read $chan $n
seek $chan $n current
return
}
# ### ### ### ######### ######### #########
##
proc ::fileutil::decode::unsigned {} {
variable val
if {$val >= 0} return
variable mask
if {$mask eq {}} {
return -code error \
-errorcode {FILE DECODE ILLEGAL UNSIGNED} \
"Unsigned not possible here"
}
set val [format %u [expr {$val & $mask}]]
return
}
proc ::fileutil::decode::match {eval} {
variable val
#puts "Match: Expected $eval, Got: [format 0x%08x $val]"
if {$val == $eval} {return 1}
rewind
return 0
}
proc ::fileutil::decode::recode {cmdpfx} {
variable val
lappend cmdpfx $val
set val [uplevel 1 $cmdpfx]
return
}
proc ::fileutil::decode::getval {} {
variable val
return $val
}
# ### ### ### ######### ######### #########
##
proc ::fileutil::decode::clear {} {
variable buf {}
return
}
proc ::fileutil::decode::get {} {
variable buf
return $buf
}
proc ::fileutil::decode::setbuf {list} {
variable buf $list
return
}
proc ::fileutil::decode::put {name} {
variable buf
variable val
lappend buf $name $val
return
}
proc ::fileutil::decode::putloc {name} {
variable buf
variable chan
lappend buf $name [tell $chan]
return
}
# ### ### ### ######### ######### #########
##
namespace eval ::fileutil::decode {
# Stream to read from
variable chan {}
# Last value read from the stream, or modified through decoder
# operations.
variable val {}
# Remembered location in the stream
variable mark {}
# Buffer for accumulating structured results
variable buf {}
# Mask for trimming a value to unsigned.
# Size-dependent
variable mask {}
}
# ### ### ### ######### ######### #########
## Ready
package provide fileutil::decode 0.2.1
return

28
src/bootsupport/fileutil/multi-0.1.tm

@ -0,0 +1,28 @@
# ### ### ### ######### ######### #########
##
# (c) 2007 Andreas Kupries.
# Multi file operations. Singleton based on the multiop processor.
# ### ### ### ######### ######### #########
## Requisites
package require fileutil::multi::op
# ### ### ### ######### ######### #########
## API & Implementation
namespace eval ::fileutil {}
# Create the multiop processor object and make its do method the main
# command of this package.
::fileutil::multi::op ::fileutil::multi::obj
proc ::fileutil::multi {args} {
return [uplevel 1 [linsert $args 0 ::fileutil::multi::obj do]]
}
# ### ### ### ######### ######### #########
## Ready
package provide fileutil::multi 0.1

645
src/bootsupport/fileutil/multi/op-0.5.3.tm

@ -0,0 +1,645 @@
# ### ### ### ######### ######### #########
##
# (c) 2007-2008 Andreas Kupries.
# DSL allowing the easy specification of multi-file copy and/or move
# and/or deletion operations. Alternate names would be scatter/gather
# processor, or maybe even assembler.
# Examples:
# (1) copy
# into [installdir_of tls]
# from c:/TDK/PrivateOpenSSL/bin
# the *.dll
#
# (2) move
# from /sources
# into /scratch
# the *
# but not *.html
# (Alternatively: except for *.html)
#
# (3) into /scratch
# from /sources
# move
# as pkgIndex.tcl
# the index
#
# (4) in /scratch
# remove
# the *.txt
# The language is derived from the parts of TclApp's option language
# dealing with files and their locations, yet not identical. In parts
# simplified, in parts more capable, keyword names were changed
# throughout.
# Language commands
# From the examples
#
# into DIR : Specify destination directory.
# in DIR : See 'into'.
# from DIR : Specify source directory.
# the PATTERN (...) : Specify files to operate on.
# but not PATTERN : Specify exceptions to 'the'.
# but exclude PATTERN : Specify exceptions to 'the'.
# except for PATTERN : See 'but not'.
# as NAME : New name for file.
# move : Move files.
# copy : Copy files.
# remove : Delete files.
#
# Furthermore
#
# reset : Force to defaults.
# cd DIR : Change destination to subdirectory.
# up : Change destination to parent directory.
# ( : Save a copy of the current state.
# ) : Restore last saved state and make it current.
# The main active element is the command 'the'. In other words, this
# command not only specifies the files to operate on, but also
# executes the operation as defined in the current state. All other
# commands modify the state to set the operation up, and nothing
# else. To allow for a more natural syntax the active command also
# looks ahead for the commands 'as', 'but', and 'except', and executes
# them, like qualifiers, so that they take effect as if they had been
# written before. The command 'but' and 'except use identical
# constructions to handle their qualifiers, i.e. 'not' and 'for'.
# Note that the fact that most commands just modify the state allows
# us to use more off forms as specifications instead of just natural
# language sentences For example the example 2 can re-arranged into:
#
# (5) from /sources
# into /scratch
# but not *.html
# move
# the *
#
# and the result is still a valid specification.
# Further note that the information collected by 'but', 'except', and
# 'as' is automatically reset after the associated 'the' was
# executed. However no other state is reset in that manner, allowing
# the user to avoid repetitions of unchanging information. Lets us for
# example merge the examples 2 and 3. The trivial merge is:
# (6) move
# into /scratch
# from /sources
# the *
# but not *.html not index
# move
# into /scratch
# from /sources
# the index
# as pkgIndex.tcl
#
# With less repetitions
#
# (7) move
# into /scratch
# from /sources
# the *
# but not *.html not index
# the index
# as pkgIndex.tcl
# I have not yet managed to find a suitable syntax to specify when to
# add a new extension to the moved/copied files, or have to strip all
# extensions, a specific extension, or even replace extensions.
# Other possibilities to muse about: Load the patterns for 'not'/'for'
# from a file ... Actually, load the whole exceptions from a file,
# with its contents a proper interpretable word list. Which makes it
# general processing of include files.
# ### ### ### ######### ######### #########
## Requisites
# This processor uses the 'wip' word list interpreter as its
# foundation.
package require fileutil ; # File testing
package require snit ; # OO support
package require struct::stack ; # Context stack
package require wip ; # DSL execution core
# ### ### ### ######### ######### #########
## API & Implementation
snit::type ::fileutil::multi::op {
# ### ### ### ######### ######### #########
## API
constructor {args} {} ; # create processor
# ### ### ### ######### ######### #########
## API - Implementation.
constructor {args} {
install stack using struct::stack ${selfns}::stack
$self wip_setup
# Mapping dsl commands to methods.
defdva \
reset Reset ( Push ) Pop \
into Into in Into from From \
cd ChDir up ChUp as As \
move Move copy Copy remove Remove \
but But not Exclude the The \
except Except for Exclude exclude Exclude \
to Into -> Save the-set TheSet \
recursive Recursive recursively Recursive \
for-win ForWindows for-unix ForUnix \
for-windows ForWindows expand Expand \
invoke Invoke strict Strict !strict NotStrict \
files Files links Links all Everything \
dirs Directories directories Directories \
state? QueryState from? QueryFrom into? QueryInto \
excluded? QueryExcluded as? QueryAs type? QueryType \
recursive? QueryRecursive operation? QueryOperation \
strict? QueryStrict !recursive NotRecursive
$self Reset
runl $args
return
}
destructor {
$mywip destroy
return
}
method do {args} {
return [runl $args]
}
# ### ### ### ######### ######### #########
## DSL Implementation
wip::dsl
# General reset of processor state
method Reset {} {
$stack clear
set base ""
set alias ""
set op ""
set recursive 0
set src ""
set excl ""
set types {}
set strict 0
return
}
# Stack manipulation
method Push {} {
$stack push [list $base $alias $op $opcmd $recursive $src $excl $types $strict]
return
}
method Pop {} {
if {![$stack size]} {
return -code error {Stack underflow}
}
foreach {base alias op opcmd recursive src excl types strict} [$stack pop] break
return
}
# Destination directory
method Into {dir} {
if {$dir eq ""} {set dir [pwd]}
if {$strict && ![fileutil::test $dir edr msg {Destination directory}]} {
return -code error $msg
}
set base $dir
return
}
method ChDir {dir} { $self Into [file join $base $dir] ; return }
method ChUp {} { $self Into [file dirname $base] ; return }
# Detail
method As {fname} {
set alias [ForceRelative $fname]
return
}
# Operations
method Move {} { set op move ; return }
method Copy {} { set op copy ; return }
method Remove {} { set op remove ; return }
method Expand {} { set op expand ; return }
method Invoke {cmdprefix} {
set op invoke
set opcmd $cmdprefix
return
}
# Operation qualifier
method Recursive {} { set recursive 1 ; return }
method NotRecursive {} { set recursive 0 ; return }
# Source directory
method From {dir} {
if {$dir eq ""} {set dir [pwd]}
if {![fileutil::test $dir edr msg {Source directory}]} {
return -code error $msg
}
set src $dir
return
}
# Exceptions
method But {} { run_next_while {not exclude} ; return }
method Except {} { run_next_while {for} ; return }
method Exclude {pattern} {
lappend excl $pattern
return
}
# Define the files to operate on, and perform the operation.
method The {pattern} {
run_next_while {as but except exclude from into in to files dirs directories links all}
switch -exact -- $op {
invoke {Invoke [Resolve [Remember [Exclude [Expand $src $pattern]]]]}
move {Move [Resolve [Remember [Exclude [Expand $src $pattern]]]]}
copy {Copy [Resolve [Remember [Exclude [Expand $src $pattern]]]]}
remove {Remove [Remember [Exclude [Expand $base $pattern]]] }
expand { Remember [Exclude [Expand $base $pattern]] }
}
# Reset the per-pattern flags of the resolution context back
# to their defaults, for the next pattern.
set alias {}
set excl {}
set recursive 0
return
}
# Like 'The' above, except that the fileset is taken from the
# specified variable. Semi-complementary to 'Save' below.
# Exclusion data and recursion info do not apply for this, this is
# already implicitly covered by the set, when it was generated.
method TheSet {varname} {
# See 'Save' for the levels we jump here.
upvar 5 $varname var
run_next_while {as from into in to}
switch -exact -- $op {
invoke {Invoke [Resolve $var]}
move {Move [Resolve $var]}
copy {Copy [Resolve $var]}
remove {Remove $var }
expand {
return -code error "Expansion does not make sense\
when we already have a set of files."
}
}
# Reset the per-pattern flags of the resolution context back
# to their defaults, for the next pattern.
set alias {}
return
}
# Save the last expansion result to a variable for use by future commands.
method Save {varname} {
# Levels to jump. Brittle.
# 5: Caller
# 4: object do ...
# 3: runl
# 2: wip::runl
# 1: run_next
# 0: Here
upvar 5 $varname v
set v $lastexpansion
return
}
# Platform conditionals ...
method ForUnix {} {
global tcl_platform
if {$tcl_platform(platform) eq "unix"} return
# Kill the remaining code. This effectively aborts processing.
replacel {}
return
}
method ForWindows {} {
global tcl_platform
if {$tcl_platform(platform) eq "windows"} return
# Kill the remaining code. This effectively aborts processing.
replacel {}
return
}
# Strictness
method Strict {} {
set strict 1
return
}
method NotStrict {} {
set strict 0
return
}
# Type qualifiers
method Files {} {
set types files
return
}
method Links {} {
set types links
return
}
method Directories {} {
set types dirs
return
}
method Everything {} {
set types {}
return
}
# State interogation
method QueryState {} {
return [list \
from $src \
into $base \
as $alias \
op $op \
excluded $excl \
recursive $recursive \
type $types \
strict $strict \
]
}
method QueryExcluded {} {
return $excl
}
method QueryFrom {} {
return $src
}
method QueryInto {} {
return $base
}
method QueryAs {} {
return $alias
}
method QueryOperation {} {
return $op
}
method QueryRecursive {} {
return $recursive
}
method QueryType {} {
return $types
}
method QueryStrict {} {
return $strict
}
# ### ### ### ######### ######### #########
## DSL State
component stack ; # State stack - ( )
variable base "" ; # Destination dir - into, in, cd, up
variable alias "" ; # Detail - as
variable op "" ; # Operation - move, copy, remove, expand, invoke
variable opcmd "" ; # Command prefix for invoke.
variable recursive 0 ; # Op. qualifier: recursive expansion?
variable src "" ; # Source dir - from
variable excl "" ; # Excluded files - but not|exclude, except for
# incl ; # Included files - the (immediate use)
variable types {} ; # Limit glob/find to specific types (f, l, d).
variable strict 0 ; # Strictness of into/Expand
variable lastexpansion "" ; # Area for last expansion result, for 'Save' to take from.
# ### ### ### ######### ######### #########
## Internal -- Path manipulation helpers.
proc ForceRelative {path} {
set pathtype [file pathtype $path]
switch -exact -- $pathtype {
relative {
return $path
}
absolute {
# Chop off the first element in the path, which is the
# root, either '/' or 'x:/'. If this was the only
# element assume an empty path.
set path [lrange [file split $path] 1 end]
if {![llength $path]} {return {}}
return [eval [linsert $path 0 file join]]
}
volumerelative {
return -code error {Unable to handle volumerelative path, yet}
}
}
return -code error \
"file pathtype returned unknown type \"$pathtype\""
}
proc ForceAbsolute {path} {
return [file join [pwd] $path]
}
# ### ### ### ######### ######### #########
## Internal - Operation execution helpers
proc Invoke {files} {
upvar 1 base base src src opcmd opcmd
uplevel #0 [linsert $opcmd end $src $base $files]
return
}
proc Move {files} {
upvar 1 base base src src
foreach {s d} $files {
set s [file join $src $s]
set d [file join $base $d]
file mkdir [file dirname $d]
file rename -force $s $d
}
return
}
proc Copy {files} {
upvar 1 base base src src
foreach {s d} $files {
set s [file join $src $s]
set d [file join $base $d]
file mkdir [file dirname $d]
if {
[file isdirectory $s] &&
[file exists $d] &&
[file isdirectory $d]
} {
# Special case: source and destination are
# directories, and the latter exists. This puts the
# source under the destination, and may even prevent
# copying at all. The semantics of the operation is
# that the source is the destination. We avoid the
# trouble by copying the contents of the source,
# instead of the directory itself.
foreach path [glob -directory $s *] {
file copy -force $path $d
}
} else {
file copy -force $s $d
}
}
return
}
proc Remove {files} {
upvar 1 base base
foreach f $files {
file delete -force [file join $base $f]
}
return
}
# ### ### ### ######### ######### #########
## Internal -- Resolution helper commands
typevariable tmap -array {
files {f TFile}
links {l TLink}
dirs {d TDir}
{} {{} {}}
}
proc Expand {dir pattern} {
upvar 1 recursive recursive strict strict types types tmap tmap
# FUTURE: struct::list filter ...
set files {}
if {$recursive} {
# Recursion through the entire directory hierarchy, save
# all matching paths.
set filter [lindex $tmap($types) 1]
if {$filter ne ""} {
set filter [myproc $filter]
}
foreach f [fileutil::find $dir $filter] {
if {![string match $pattern [file tail $f]]} continue
lappend files [fileutil::stripPath $dir $f]
}
} else {
# No recursion, just scan the whole directory for matching paths.
# check for specific types integrated.
set filter [lindex $tmap($types) 0]
if {$filter ne ""} {
foreach f [glob -nocomplain -directory $dir -types $filter -- $pattern] {
lappend files [fileutil::stripPath $dir $f]
}
} else {
foreach f [glob -nocomplain -directory $dir -- $pattern] {
lappend files [fileutil::stripPath $dir $f]
}
}
}
if {[llength $files]} {return $files}
if {!$strict} {return {}}
return -code error \
"No files matching pattern \"$pattern\" in directory \"$dir\""
}
proc TFile {f} {file isfile $f}
proc TDir {f} {file isdirectory $f}
proc TLink {f} {expr {[file type $f] eq "link"}}
proc Exclude {files} {
upvar 1 excl excl
# FUTURE: struct::list filter ...
set res {}
foreach f $files {
if {[IsExcluded $f $excl]} continue
lappend res $f
}
return $res
}
proc IsExcluded {f patterns} {
foreach p $patterns {
if {[string match $p $f]} {return 1}
}
return 0
}
proc Resolve {files} {
upvar 1 alias alias
set res {}
foreach f $files {
# Remember alias for processing and auto-invalidate to
# prevent contamination of the next file.
set thealias $alias
set alias ""
if {$thealias eq ""} {
set d $f
} else {
set d [file dirname $f]
if {$d eq "."} {
set d $thealias
} else {
set d [file join $d $thealias]
}
}
lappend res $f $d
}
return $res
}
proc Remember {files} {
upvar 1 lastexpansion lastexpansion
set lastexpansion $files
return $files
}
##
# ### ### ### ######### ######### #########
}
# ### ### ### ######### ######### #########
## Ready
package provide fileutil::multi::op 0.5.3

74
src/bootsupport/fileutil/paths-1.tm

@ -0,0 +1,74 @@
# paths.tcl --
#
# Manage lists of search paths.
#
# Copyright (c) 2009-2019 Andreas Kupries <andreas_kupries@sourceforge.net>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# Each object instance manages a list of paths.
# ### ### ### ######### ######### #########
## Requisites
package require Tcl 8.4
package require snit
# ### ### ### ######### ######### #########
## API
snit::type ::fileutil::paths {
# ### ### ### ######### ######### #########
## Options :: None
# ### ### ### ######### ######### #########
## Creation, destruction
# Default constructor.
# Default destructor.
# ### ### ### ######### ######### #########
## Methods :: Querying and manipulating the list of paths.
method paths {} {
return $mypaths
}
method add {path} {
set pos [lsearch $mypaths $path]
if {$pos >= 0 } return
lappend mypaths $path
return
}
method remove {path} {
set pos [lsearch $mypaths $path]
if {$pos < 0} return
set mypaths [lreplace $mypaths $pos $pos]
return
}
method clear {} {
set mypaths {}
return
}
# ### ### ### ######### ######### #########
## Internal methods :: None
# ### ### ### ######### ######### #########
## State :: List of paths.
variable mypaths {}
##
# ### ### ### ######### ######### #########
}
# ### ### ### ######### ######### #########
## Ready
package provide fileutil::paths 1
return

504
src/bootsupport/fileutil/traverse-0.6.tm

@ -0,0 +1,504 @@
# traverse.tcl --
#
# Directory traversal.
#
# Copyright (c) 2006-2015 by Andreas Kupries <andreas_kupries@users.sourceforge.net>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
package require Tcl 8.3
# OO core
if {[package vsatisfies [package present Tcl] 8.5]} {
# Use new Tcl 8.5a6+ features to specify the allowed packages.
# We can use anything above 1.3. This means v2 as well.
package require snit 1.3-
} else {
# For Tcl 8.{3,4} only snit1 of a suitable patchlevel is possible.
package require snit 1.3
}
package require control ; # Helpers for control structures
package require fileutil ; # -> fullnormalize
snit::type ::fileutil::traverse {
# Incremental directory traversal.
# API
# create %AUTO% basedirectory options... -> object
# next filevar -> boolean
# foreach filevar script
# files -> list (path ...)
# Options
# -prefilter command-prefix
# -filter command-prefix
# -errorcmd command-prefix
# Use cases
#
# (a) Basic incremental
# - Create and configure a traversal object.
# - Execute 'next' to retrieve one path at a time,
# until the command returns False, signaling that
# the iterator has exhausted the supply of paths.
# (The path is stored in the named variable).
#
# The execution of 'next' can be done in a loop, or via event
# processing.
# (b) Basic loop
# - Create and configure a traversal object.
# - Run a script for each path, using 'foreach'.
# This is a convenient standard wrapper around 'next'.
#
# The loop properly handles all possible Tcl result codes.
# (c) Non-incremental, non-looping.
# - Create and configure a traversal object.
# - Retrieve a list of all paths via 'files'.
# The -prefilter callback is executed for directories. Its result
# determines if the traverser recurses into the directory or not.
# The default is to always recurse into all directories. The call-
# back is invoked with a single argument, the path of the
# directory.
#
# The -filter callback is executed for all paths. Its result
# determines if the current path is a valid result, and returned
# by 'next'. The default is to accept all paths as valid. The
# callback is invoked with a single argument, the path to check.
# The -errorcmd callback is executed for all paths the traverser
# has trouble with. Like being unable to cd into them, get their
# status, etc. The default is to ignore any such problems. The
# callback is invoked with a two arguments, the path for which the
# error occured, and the error message. Errors thrown by the
# filter callbacks are handled through this callback too. Errors
# thrown by the error callback itself are not caught and ignored,
# but allowed to pass to the caller, usually of 'next'.
# Note: Low-level functionality, version and platform dependent is
# implemented in procedures, and conditioally defined for optimal
# use of features, etc. ...
# Note: Traversal is done in depth-first pre-order.
# Note: The options are handled only during
# construction. Afterward they are read-only and attempts to
# modify them will cause the system to throw errors.
# ### ### ### ######### ######### #########
## Implementation
option -filter -default {} -readonly 1
option -prefilter -default {} -readonly 1
option -errorcmd -default {} -readonly 1
constructor {basedir args} {
set _base $basedir
$self configurelist $args
return
}
method files {} {
set files {}
$self foreach f {lappend files $f}
return $files
}
method foreach {fvar body} {
upvar 1 $fvar currentfile
# (Re-)initialize the traversal state on every call.
$self Init
while {[$self next currentfile]} {
set code [catch {uplevel 1 $body} result]
# decide what to do upon the return code:
#
# 0 - the body executed successfully
# 1 - the body raised an error
# 2 - the body invoked [return]
# 3 - the body invoked [break]
# 4 - the body invoked [continue]
# everything else - return and pass on the results
#
switch -exact -- $code {
0 {}
1 {
return -errorinfo [::control::ErrorInfoAsCaller uplevel foreach] \
-errorcode $::errorCode -code error $result
}
3 {
# FRINK: nocheck
return
}
4 {}
default {
return -code $code $result
}
}
}
return
}
method next {fvar} {
upvar 1 $fvar currentfile
# Initialize on first call.
if {!$_init} {
$self Init
}
# We (still) have valid paths in the result stack, return the
# next one.
if {[llength $_results]} {
set top [lindex $_results end]
set _results [lreplace $_results end end]
set currentfile $top
return 1
}
# Take the next directory waiting in the processing stack and
# fill the result stack with all valid files and sub-
# directories contained in it. Extend the processing queue
# with all sub-directories not yet seen already (!circular
# symlinks) and accepted by the prefilter. We stop iterating
# when we either have no directories to process anymore, or
# the result stack contains at least one path we can return.
while {[llength $_pending]} {
set top [lindex $_pending end]
set _pending [lreplace $_pending end end]
# Directory accessible? Skip if not.
if {![ACCESS $top]} {
Error $top "Inacessible directory"
continue
}
# Expand the result stack with all files in the directory,
# modulo filtering.
foreach f [GLOBF $top] {
if {![Valid $f]} continue
lappend _results $f
}
# Expand the result stack with all sub-directories in the
# directory, modulo filtering. Further expand the
# processing stack with the same directories, if not seen
# yet and modulo pre-filtering.
foreach f [GLOBD $top] {
if {
[string equal [file tail $f] "."] ||
[string equal [file tail $f] ".."]
} continue
if {[Valid $f]} {
lappend _results $f
}
Enter $top $f
if {[Cycle $f]} continue
if {[Recurse $f]} {
lappend _pending $f
}
}
# Stop expanding if we have paths to return.
if {[llength $_results]} {
set top [lindex $_results end]
set _results [lreplace $_results end end]
set currentfile $top
return 1
}
}
# Allow re-initialization with next call.
set _init 0
return 0
}
# ### ### ### ######### ######### #########
## Traversal state
# * Initialization flag. Checked in 'next', reset by next when no
# more files are available. Set in 'Init'.
# * Base directory (or file) to start the traversal from.
# * Stack of prefiltered unknown directories waiting for
# processing, i.e. expansion (TOP at end).
# * Stack of valid paths waiting to be returned as results.
# * Set of directories already visited (normalized paths), for
# detection of circular symbolic links.
variable _init 0 ; # Initialization flag.
variable _base {} ; # Base directory.
variable _pending {} ; # Processing stack.
variable _results {} ; # Result stack.
# sym link handling (to break cycles, while allowing the following of non-cycle links).
# Notes
# - path parent tracking is lexical.
# - path identity tracking is based on the normalized path, i.e. the path with all
# symlinks resolved.
# Maps
# - path -> parent (easier to follow the list than doing dirname's)
# - path -> normalized (cache to avoid redundant calls of fullnormalize)
# cycle <=> A parent's normalized form (NF) is identical to the current path's NF
variable _parent -array {}
variable _norm -array {}
# ### ### ### ######### ######### #########
## Internal helpers.
proc Enter {parent path} {
#puts ___E|$path
upvar 1 _parent _parent _norm _norm
set _parent($path) $parent
set _norm($path) [fileutil::fullnormalize $path]
}
proc Cycle {path} {
upvar 1 _parent _parent _norm _norm
set nform $_norm($path)
set paren $_parent($path)
while {$paren ne {}} {
if {$_norm($paren) eq $nform} { return yes }
set paren $_parent($paren)
}
return no
}
method Init {} {
array unset _parent *
array unset _norm *
# Path ok as result?
if {[Valid $_base]} {
lappend _results $_base
}
# Expansion allowed by prefilter?
if {[file isdirectory $_base] && [Recurse $_base]} {
Enter {} $_base
lappend _pending $_base
}
# System is set up now.
set _init 1
return
}
proc Valid {path} {
#puts ___V|$path
upvar 1 options options
if {![llength $options(-filter)]} {return 1}
set path [file normalize $path]
set code [catch {uplevel \#0 [linsert $options(-filter) end $path]} valid]
if {!$code} {return $valid}
Error $path $valid
return 0
}
proc Recurse {path} {
#puts ___X|$path
upvar 1 options options _norm _norm
if {![llength $options(-prefilter)]} {return 1}
set path [file normalize $path]
set code [catch {uplevel \#0 [linsert $options(-prefilter) end $path]} valid]
if {!$code} {return $valid}
Error $path $valid
return 0
}
proc Error {path msg} {
upvar 1 options options
if {![llength $options(-errorcmd)]} return
set path [file normalize $path]
uplevel \#0 [linsert $options(-errorcmd) end $path $msg]
return
}
##
# ### ### ### ######### ######### #########
}
# ### ### ### ######### ######### #########
##
# The next three helper commands for the traverser depend strongly on
# the version of Tcl, and partially on the platform.
# 1. In Tcl 8.3 using -types f will return only true files, but not
# links to files. This changed in 8.4+ where links to files are
# returned as well. So for 8.3 we have to handle the links
# separately (-types l) and also filter on our own.
# Note that Windows file links are hard links which are reported by
# -types f, but not -types l, so we can optimize that for the two
# platforms.
#
# 2. In Tcl 8.3 we also have a crashing bug in glob (SIGABRT, "stat on
# a known file") when trying to perform 'glob -types {hidden f}' on
# a directory without e'x'ecute permissions. We code around by
# testing if we can cd into the directory (stat might return enough
# information too (mode), but possibly also not portable).
#
# For Tcl 8.2 and 8.4+ glob simply delivers an empty result
# (-nocomplain), without crashing. For them this command is defined
# so that the bytecode compiler removes it from the bytecode.
#
# This bug made the ACCESS helper necessary.
# We code around the problem by testing if we can cd into the
# directory (stat might return enough information too (mode), but
# possibly also not portable).
if {[package vsatisfies [package present Tcl] 8.5]} {
# Tcl 8.5+.
# We have to check readability of "current" on our own, glob
# changed to error out instead of returning nothing.
proc ::fileutil::traverse::ACCESS {args} {return 1}
proc ::fileutil::traverse::GLOBF {current} {
if {![file readable $current] ||
[BadLink $current]} {
return {}
}
set res [lsort -unique [concat \
[glob -nocomplain -directory $current -types f -- *] \
[glob -nocomplain -directory $current -types {hidden f} -- *]]]
# Look for broken links (They are reported as neither file nor directory).
foreach l [lsort -unique [concat \
[glob -nocomplain -directory $current -types l -- *] \
[glob -nocomplain -directory $current -types {hidden l} -- *]]] {
if {[file isfile $l]} continue
if {[file isdirectory $l]} continue
lappend res $l
}
return [lsort -unique $res]
}
proc ::fileutil::traverse::GLOBD {current} {
if {![file readable $current] ||
[BadLink $current]} {
return {}
}
lsort -unique [concat \
[glob -nocomplain -directory $current -types d -- *] \
[glob -nocomplain -directory $current -types {hidden d} -- *]]
}
proc ::fileutil::traverse::BadLink {current} {
if {[file type $current] ne "link"} { return no }
set dst [file join [file dirname $current] [file readlink $current]]
if {![file exists $dst] ||
![file readable $dst]} {
return yes
}
return no
}
} elseif {[package vsatisfies [package present Tcl] 8.4]} {
# Tcl 8.4+.
# (Ad 1) We have -directory, and -types,
# (Ad 2) Links are returned for -types f/d if they refer to files/dirs.
# (Ad 3) No bug to code around
proc ::fileutil::traverse::ACCESS {args} {return 1}
proc ::fileutil::traverse::GLOBF {current} {
set res [concat \
[glob -nocomplain -directory $current -types f -- *] \
[glob -nocomplain -directory $current -types {hidden f} -- *]]
# Look for broken links (They are reported as neither file nor directory).
foreach l [concat \
[glob -nocomplain -directory $current -types l -- *] \
[glob -nocomplain -directory $current -types {hidden l} -- *] ] {
if {[file isfile $l]} continue
if {[file isdirectory $l]} continue
lappend res $l
}
return $res
}
proc ::fileutil::traverse::GLOBD {current} {
concat \
[glob -nocomplain -directory $current -types d -- *] \
[glob -nocomplain -directory $current -types {hidden d} -- *]
}
} else {
# 8.3.
# (Ad 1) We have -directory, and -types,
# (Ad 2) Links are NOT returned for -types f/d, collect separately.
# No symbolic file links on Windows.
# (Ad 3) Bug to code around.
proc ::fileutil::traverse::ACCESS {current} {
if {[catch {
set h [pwd] ; cd $current ; cd $h
}]} {return 0}
return 1
}
if {[string equal $::tcl_platform(platform) windows]} {
proc ::fileutil::traverse::GLOBF {current} {
concat \
[glob -nocomplain -directory $current -types f -- *] \
[glob -nocomplain -directory $current -types {hidden f} -- *]]
}
} else {
proc ::fileutil::traverse::GLOBF {current} {
set l [concat \
[glob -nocomplain -directory $current -types f -- *] \
[glob -nocomplain -directory $current -types {hidden f} -- *]]
foreach x [concat \
[glob -nocomplain -directory $current -types l -- *] \
[glob -nocomplain -directory $current -types {hidden l} -- *]] {
if {[file isdirectory $x]} continue
# We have now accepted files, links to files, and broken links.
lappend l $x
}
return $l
}
}
proc ::fileutil::traverse::GLOBD {current} {
set l [concat \
[glob -nocomplain -directory $current -types d -- *] \
[glob -nocomplain -directory $current -types {hidden d} -- *]]
foreach x [concat \
[glob -nocomplain -directory $current -types l -- *] \
[glob -nocomplain -directory $current -types {hidden l} -- *]] {
if {![file isdirectory $x]} continue
lappend l $x
}
return $l
}
}
# ### ### ### ######### ######### #########
## Ready
package provide fileutil::traverse 0.6

789
src/bootsupport/punk/du-0.1.0.tm

@ -0,0 +1,789 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punk::du 0.1.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
namespace eval punk::du {
variable has_twapi 0
}
if {"windows" eq $::tcl_platform(platform)} {
if {[catch {package require twapi}]} {
puts stderr "Warning: punk::du - unable to load twapi. Disk operations may be much slower on windows without the twapi package"
} else {
set punk::du::has_twapi 1
}
package require punk::winpath
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::du {
proc dirlisting {{folderpath {}}} {
if {[lib::pathcharacterlen $folderpath] == 0} {
set folderpath [pwd]
} elseif {[file pathtype $folderpath] ne "absolute"} {
#file normalize relativelly slow - avoid in inner loops
#set folderpath [file normalize $folderpath]
}
#run whichever of du_dirlisting_twapi, du_dirlisting_generic, du_dirlisting_unix has been activated
set dirinfo [active::du_dirlisting $folderpath]
}
#Note that unix du seems to do depth-first - which makese sense when piping.. as output can be emitted as we go rather than requiring sort at end.
#breadth-first with sort can be quite fast .. but memory usage can easily get out of control
proc du { args } {
variable has_twapi
package require struct::set
if 0 {
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 opts $args
# flags in args are solos (or longopts --something=somethingelse) or sometimes pairopts
# we don't currently support mashopts (ie -xy vs separate -x -y)
#-------------------------------------------------------
# process any pairopts first and remove the pair
# (may also process some solo-opts)
set opt_depth -1
if {[set posn [lsearch $opts -d]] >= 0} {
set opt_depth [lindex $opts $posn+1]
set opts [lreplace $opts $posn $posn+1]
}
foreach o $opts {
if {[string match --max-depth=* $o]} {
set opt_depth [lindex [split $o =] 1]
if {![string is integer -strict $opt_depth]} {
error "--max-depth=n n must be an integer"
}
}
}
#-------------------------------------------------------
#only solos and longopts remain in the opts now
set lastarg [lindex $opts end]
if {[string length $lastarg] && (![string match -* $lastarg])} {
set dir $lastarg
set opts [lrange $opts 0 end-1]
} else {
set dir .
set opts $opts
}
foreach a $opts {
if {![string match -* $a]} {
error "unrecognized option '$a'"
}
}
set -b 1
set -k 1024
set -m [expr 1024*1024]
set switch -k ;#default (same as unix)
set lc_opts [string tolower $opts]
if {"-b" in $lc_opts} {
set switch -b
} elseif {"-k" in $lc_opts} {
set switch -k
} elseif {"-m" in $lc_opts} {
set switch -m
}
set opt_progress 0
if {"--prog" in $lc_opts || "--progress" in $lc_opts} {
set opt_progress 1
}
set opt_extra 0
if {"--extra" in $lc_opts} {
set opt_extra 1
}
set opt_vfs 0
if {"--vfs" in $lc_opts} {
set opt_vfs 1
}
set result [list]
set dir_depths_remaining [list]
set is_windows [expr {$::tcl_platform(platform) eq "windows"}]
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}] [lib::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}] [lib::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 0
set levelfiles 0
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]
#puts stderr ->$folderpath
#if {$i >= 20} {
#return
#}
#twapi supports gathering file sizes during directory contents traversal
#for dirlisting methods that return an empty list in filesizes whilst files has entries - we will need to populate it below
#e.g tcl glob based dirlisting doesn't support gathering file sizes at the same time
set in_vfs 0
if {$opt_vfs} {
foreach vfsmount [vfs::filesystem info] {
if {[punk::repo::path_a_atorbelow_b $folderpath $vfsmount]} {
set in_vfs 1
break
}
}
}
if {$in_vfs} {
set du_info [lib::du_dirlisting_tclvfs $folderpath]
} else {
#run the activated function (proc imported to active namespace and renamed)
set du_info [active::du_dirlisting $folderpath]
}
set dirs [dict get $du_info dirs]
set files [dict get $du_info files]
set filesizes [dict get $du_info filesizes]
incr leveldirs [llength $dirs]
incr levelfiles [llength $files]
#lappend dir_depths_remaining {*}[lmap d $dirs {::list $nextdepth [lib::du_lit $cont/$itm] $d $zero}]
#folderidx is parent index for new dirs
lappend dir_depths_remaining {*}[lib::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}]
if {[llength $files] && ![llength $filesizes]} {
#listing mechanism didn't supply corresponding sizes
foreach filename $files {
#incr bytecount [file size [file join $folderpath $filename]
incr bytecount [file size $filename]
}
} else {
set filesizes [lsearch -all -inline -not $filesizes[unset filesizes] na] ;#only legal non-number is na
set bytecount [tcl::mathop::+ {*}$filesizes]
}
#we can safely assume initial count was zero
lset dir_depths_remaining $i $i_size $bytecount
#incr diridx
}
#puts stdout "level: $level dirs: $leveldirs"
if {$opt_extra} {
puts stdout "level: $level dircount: $leveldirs filecount: $levelfiles"
}
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 but significant on large tree - the real time is for large trees in the main loop above
#update - on really large trees the reverse is true especiallyl now that twapi fixed the original speed issues.. todo - rework/simplify below - review natsort
#
#TODO - reconsider sorting by depth.. lreverse dirs_as_encountered should work..
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]
#
set retval [list]
#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 depth [lindex $dirinfo $i_depth]
if {($opt_depth >= 0) && $depth > $opt_depth} {
continue
}
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
}
namespace eval active {
variable functions [list du_dirlisting ""]
variable functions_known [dict create]
#known functions from lib namespace
dict set functions_known du_dirlisting [list du_dirlisting_twapi du_dirlisting_generic du_dirlisting_unix]
proc show_functions {} {
variable functions
variable functions_known
set msg ""
dict for {callname implementations} $functions_known {
append msg "callname: $callname" \n
foreach imp $implementations {
if {[dict get $functions $callname] eq $imp} {
append msg " $imp (active)" \n
} else {
append msg " $imp" \n
}
}
}
return $msg
}
proc set_active_function {callname implementation} {
variable functions
variable functions_known
if {$callname ni [dict keys $functions_known]} {
error "unknown function callname $callname"
}
if {$implementation ni [dict get $functions_known $callname]} {
error "unknown implementation $implementation for callname $callname"
}
dict set functions $callname $implementation
catch {rename ::punk::du::active::$callname ""}
namespace eval ::punk::du::active [string map [list %imp% $implementation %call% $callname] {
namespace import ::punk::du::lib::%imp%
rename %imp% %call%
}]
return $implementation
}
proc get_active_function {callname} {
variable functions
variable functions_known
if {$callname ni [dict keys $functions_known]} {
error "unknown function callname $callname known functions: [dict keys $functions_known]"
}
return [dict get $functions $callname]
}
#where we import & the appropriate du_listing.. function for the platform
}
namespace eval lib {
variable du_literal
variable winfile_attributes [list 16 directory 32 archive 1024 reparse_point 18 [list directory hidden] 34 [list archive hidden] ]
#caching this is faster than calling twapi api each time.. unknown if twapi is calculating from bitmask - or calling windows api
#we could work out all flags and calculate from bitmask.. but it's not necessarily going to be faster than some simple caching mechanism like this
namespace export du_dirlisting_twapi du_dirlisting_generic du_dirlisting_unix
# get listing without using unix-tools (may not be installed on the windows system)
# this dirlisting is customised for du - so only retrieves dirs,files,filesizes (minimum work needed to perform du function)
proc du_dirlisting_twapi {folderpath} {
set altname "" ;#possible we have to use a different name e.g short windows name or dos-device path //?/
# return it so it can be stored and tried as an alternative for problem paths
try {
set iterator [twapi::find_file_open [file join $folderpath *] -detail basic] ;# -detail full only adds data to the altname field
} on error args {
try {
if {[string match "*denied*" $args]} {
#output similar format as unixy du
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
if {[string match "*TWAPI_WIN32 59*" $::errorCode]} {
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args (possibly blocked by permissions or share config e.g follow symlinks = no on samba)"
puts stderr " (errorcode: $::errorCode)\n"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
if {[set plen [pathcharacterlen $folderpath]] >= 250} {
set errmsg "error reading folder: $folderpath (len:$plen)\n"
append errmsg "error: $args" \n
append errmsg "errorcode: $::errorCode" \n
# re-fetch this folder with altnames
#file normalize - aside from being slow - will have problems with long paths - so this won't work.
#this function should only accept absolute paths
#
#
#Note: using -detail full only helps if the last segment of path has an altname..
#To properly shorten we need to have kept track of altname all the way from the root!
#We can .. for now call Tcl's file attributes to get shortname of the whole path - it is *expensive* e.g 5ms for a long path on local ssd
#### SLOW
set fixedpath [dict get [file attributes $folderpath] -shortname]
#### SLOW
append errmsg "retrying with with windows altname '$fixedpath'"
puts stderr $errmsg
} else {
set errmsg "error reading folder: $folderpath (len:$plen)\n"
append errmsg "error: $args" \n
append errmsg "errorcode: $::errorCode" \n
#possibly an illegal windows filename - easily happens on a machine with WSL or with drive mapped to unix share
#we can use //?/path dos device path - but not with tcl functions
#unfortunately we can't call find_file_open directly on the problem name - we have to call the parent folder and iterate through again..
#this gets problematic as we go deeper unless we rewrite the .. but we can get at least one level further here
set parent [file dirname $folderpath]
set badtail [file tail $folderpath]
set iterator [twapi::find_file_open [file join $parent *] -detail full] ;#retrieve with altnames
set fixedtail ""
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
if {$nm eq $badtail} {
set fixedtail [dict get $iteminfo altname]
break
}
}
if {![string length $fixedtail]} {
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args (Unable to retrieve altname to progress further with path - returning no contents for this folder)"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
#twapi as at 2023-08 doesn't seem to support //?/ dos device paths..
#Tcl can test only get as far as testing existence of illegal name by prefixing with //?/ - but can't glob inside it
#we can call file attributes on it - but we get no shortname (but we could get shortname for parent that way)
#so the illegalname_fix doesn't really work here
#set fixedpath [punk::winpath::illegalname_fix $parent $fixedtail]
#this has shortpath for the tail - but it's not the canonical-shortpath because we didn't call it on the $parent part REIEW.
set fixedpath [file join $parent $fixedtail]
append errmsg "retrying with with windows dos device path $fixedpath\n"
puts stderr $errmsg
}
set iterator [twapi::find_file_open $fixedpath/* -detail basic]
} on error args {
set errmsg "error reading folder: $folderpath\n"
append errmsg "error: $args"
append errmsg "aborting.."
error $errmsg
}
}
set dirs [list]
set files [list]
set filesizes [list]
set links [list]
set flaggedhidden [list]
set flaggedsystem [list]
set flaggedreadonly [list]
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
set tail_altname [dict get $iteminfo altname] ;#altname of tail - not whole path
set attrinfo [decode_win_attributes [dict get $iteminfo attrs]]
#puts stderr "$iteminfo"
#puts stderr "$nm -> [dict get $iteminfo attrs] -> $attrinfo"
#attributes applicable to any classification
set fullname [file_join_one $folderpath $nm]
if {"hidden" in $attrinfo} {
lappend flaggedhidden $fullname
}
if {"system" in $attrinfo} {
lappend flaggedsystem $fullname
}
if {"readonly" in $attrinfo} {
lappend flaggedreadonly $fullname
}
#main classification
if {"reparse_point" in $attrinfo} {
#we will treat as zero sized for du purposes.. review - option -L for symlinks like BSD du?
#Note 'file readlink' can fail on windows - reporting 'invalid argument' - according to tcl docs, 'On systems that don't support symbolic links this option is undefined'
#The link may be viewable ok in windows explorer, and cmd.exe /c dir and unix tools such as ls
#if we need it without resorting to unix-tools that may not be installed: exec {*}[auto_execok dir] /A:L {c:\some\path}
#e.g (stripped of headers/footers and other lines)
#2022-10-02 04:07 AM <SYMLINKD> priv [\\?\c:\repo\elixir\gameportal\apps\test\priv]
#Note we will have to parse beyond header fluff as /B strips the symlink info along with headers.
#du includes the size of the symlink
#but we can't get it with tcl's file size
#twapi doesn't seem to have anything to help read it either (?)
#the above was verified with a symlink that points to a non-existant folder.. mileage may vary for an actually valid link
#
#Note also - a shortcut created in explorer with drag and drop to an existant folder is a different animal to a symlink (file with .lnk extension) even though it looks the same in explorer window.
#
#links are techically files too, whether they point to a file/dir or nothing.
lappend links $fullname
} elseif {"directory" in $attrinfo} {
if {$nm in {. ..}} {
continue
}
lappend dirs $fullname
} else {
#review - is anything that isn't a reparse_point or a directory, some sort of 'file' in this context? What about the 'device' attribute? Can that occur in a directory listing of some sort?
lappend files $fullname
lappend filesizes [dict get $iteminfo size]
}
}
twapi::find_file_close $iterator
set vfsmounts [get_vfsmounts_in_folder $folderpath]
#also determine whether vfs. file system x is *much* faster than file attributes
#whether or not there is a corresponding file/dir add any applicable mountpoints for the containing folder
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes $filesizes flaggedhidden $flaggedhidden flaggedsystem $flaggedsystem flaggedreadonly $flaggedreadonly altname $altname]
}
proc get_vfsmounts_in_folder {folderpath} {
set vfsmounts [list]
set known_vfs_mounts [vfs::filesystem info]
foreach mount $known_vfs_mounts {
if {[punk::repo::path_a_above_b $folderpath $mount]} {
if {([llength [file split $mount]] - [llength [file split $folderpath]]) == 1} {
#the mount is in this folder
lappend vfsmounts $mount
}
}
}
return $vfsmounts
}
#work around the horrible tilde-expansion thing (not needed for tcl 9+)
proc file_join_one {base newtail} {
if {[string index $newtail 0] ne {~}} {
return [file join $base $newtail]
}
return [file join $base ./$newtail]
}
#this is the cross-platform pure-tcl version - which calls glob multiple times to make sure it gets everythign it needs and can ignore everything it needs to.
#These repeated calls to glob will be a killer for performance - especially on a network share or when walking a large directory structure
proc du_dirlisting_generic {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} * .*]
#set hdirs {}
set dirs [glob -nocomplain -dir $folderpath -types d * .*]
set hlinks [glob -nocomplain -dir $folderpath -types {hidden l} * .*]
#set hlinks {}
set links [glob -nocomplain -dir $folderpath -types l * .*] ;#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} * .*]
#set hfiles {}
set files [glob -nocomplain -dir $folderpath -types f * .*]
#set files {}
#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 [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set links [lsort -unique [concat $links $hlinks]]
if {"windows" eq $::tcl_platform(platform)} {
set flaggedhidden [concat $hdirs $hfiles $hlinks]
} else {
#unix dotted files/folders are not 'flagged' as such - it's a convention - so the client can use the same convention to decide if something is hidden
#this allows us to return less data - but more importantly - reserve flaggedhidden for systems where such a flag exists and avoid conflating the different concepts of what is hidden
set flaggedhidden {}
}
set vfsmounts [get_vfsmounts_in_folder $folderpath]
set filesizes [list]; #not available in listing-call - as opposed to twapi which can do it as it goes
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
#we can halve the number of round trips on unix-like systems, where 'hidden' always corresponds to dotted files
proc du_dirlisting_unix {folderpath} {
set dirs [glob -nocomplain -dir $folderpath -types d * .*] ;# also returns links to dirs
set links [glob -nocomplain -dir $folderpath -types l * .*] ;# links may have dupes - we don't care. struct::set difference will remove
set files [glob -nocomplain -dir $folderpath -types f * .*] ;# also returns links to files
#remove any links from our dirs and files collections
set dirs [struct::set difference $dirs[unset dirs] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference $files[unset files] $links]
set vfsmounts [get_vfsmounts_in_folder $folderpath]
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
proc du_dirlisting_tclvfs {folderpath} {
set dirs [glob -nocomplain -dir $folderpath -types d * .*] ;# also returns links to dirs
#review - how are links handled in vfs? presumably if the vfs is a full implementation they should work at least within the vfs?
set links [glob -nocomplain -dir $folderpath -types l * .*] ;# links may have dupes - we don't care. struct::set difference will remove
set files [glob -nocomplain -dir $folderpath -types f * .*] ;# also returns links to files
#remove any links from our dirs and files collections
set dirs [struct::set difference $dirs[unset dirs] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference $files[unset files] $links]
#nested vfs mount.. REVIEW - does anything need special handling?
set vfsmounts [get_vfsmounts_in_folder $folderpath]
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
proc decode_win_attributes {bitmask} {
variable winfile_attributes
if {[dict exists $winfile_attributes $bitmask]} {
return [dict get $winfile_attributes $bitmask]
} else {
#list/dict shimmering?
return [lindex [lappend winfile_attributes $bitmask [twapi::decode_file_attributes $bitmask]] end]
}
}
proc du_lit value {
variable du_literal
if {![info exists du_literal($value)]} {
set du_literal($value) $value
}
return $du_literal($value)
}
#v1
proc du_new_eachdirtail {dirtails depth parentfolderidx} {
set newlist {}
upvar folders folders
set parentpath [lindex $folders $parentfolderidx]
set newindex [llength $folders]
foreach dt $dirtails {
lappend folders [file join $parentpath [du_lit $dt]]; #store as a 'path' rather than a string (see tcl::unsupported::representation)
lappend newlist [::list $depth $parentfolderidx $newindex [du_lit $dt] [expr {0}]]
incr newindex
}
return $newlist
}
proc du_new_eachdir {dirpaths depth parentfolderidx} {
set newlist {}
upvar folders folders
set newindex [llength $folders]
foreach dp $dirpaths {
lappend folders $dp
#puts stdout "--->$dp"
lappend newlist [::list $depth $parentfolderidx $newindex [du_lit [file tail $dp]] [expr {0}]]
incr newindex
}
return $newlist
}
#just an experiment
#get length of path which has internal rep of path - maintaining path/list rep without shimmering to string representation.
proc pathcharacterlen {pathrep} {
set l 0
set parts [file split $pathrep]
if {[llength $parts] < 2} {
return [string length [lindex $parts 0]]
}
foreach seg $parts {
incr l [string length $seg]
}
return [expr {$l + [llength $parts] -2}]
}
#slower - doesn't work for short paths like c:/
proc pathcharacterlen2 {pathrep} {
return [tcl::mathop::+ {*}[lmap v [set plist [file split $pathrep]] {[string length $v]}] [llength $plist] -2]
}
#Strip using lengths without examining path components
#without normalization is much faster
proc path_strip_alreadynormalized_prefixdepth {path prefix} {
set tail [lrange [file split $path] [llength [file split $prefix]] end]
if {[llength $tail]} {
return [file join {*}$tail]
} else {
return ""
}
}
}
package require natsort
#interp alias {} du {} .=args>* punk::du |> .=>1 natsort::sort -cols 1 |> list_as_lines <args|
#use natsort -debug 2 to see index output
#this works better for display of directory/file names with spaces (doesn't show curly braces)
interp alias {} du2 {} .=args>* punk::du::du |> .=>1 natsort::sort -cols 1 -outputformat words |> list_as_lines <args|
#experiment with csv as easy way to get column like format..
#The /r is somewhat cheating however.. as it messes up redirected output .. e.g if redirected to text file
interp alias {} du {} .=args>* punk::du::du |> .=>1 natsort::sort -cols 1 -outputformat csv -outputformatoptions {\r\t\t\t} |> list_as_lines <args|
}
namespace eval ::punk::du::active {
variable functions
variable functions_kown
if {"windows" eq $::tcl_platform(platform)} {
if {$punk::du::has_twapi} {
set_active_function du_dirlisting du_dirlisting_twapi
} else {
set_active_function du_dirlisting du_dirlisting_generic
}
} else {
set_active_function du_dirlisting du_dirlisting_unix
}
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::du [namespace eval punk::du {
variable version
set version 0.1.0
}]
return

1375
src/bootsupport/punk/mix-0.2.tm

File diff suppressed because it is too large Load Diff

814
src/bootsupport/punk/repo-0.1.0.tm

@ -0,0 +1,814 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#Copyright (c) 2023 Julian Noble
#Copyright (c) 2012-2018 Andreas Kupries
# - code from A.K's 'kettle' project used in this module
#
# @@ Meta Begin
# Application punk::repo 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#
# path/repo functions
#
if {$::tcl_platform(platform) eq "windows"} {
package require punk::winpath
} else {
catch {package require punk::winpath}
}
package require cksum ;#tcllib
package require fileutil; #tcllib
# -- --- --- --- --- --- --- --- --- --- ---
# For performance/efficiency reasons - use file functions on paths in preference to string operations
# e.g use file join
# branch to avoid unnecessary calls to 'pwd' or 'file normalize' - which can be surprisingly expensive operations (as at tcl 8.7 2023)
# pwd is only expensive if we treat it as a string instead of a list/path
# e.g
# > time {set x [pwd]}
# 5 microsoeconds.. no problem
# > time {set x [pwd]}
# 4 microsoeconds.. still no problem
# > string length $x
# 45
# > time {set x [pwd]}
# 1372 microseconds per iteration ;#!! values above 0.5ms common.. and that's a potential problem in loops that trawl filesystem
# The same sorts of timings occur with file normalize
# also.. even if we build up a path with file join from a base value that has already been normalized - the subsequent normalize will be expensive
# -- --- --- --- --- --- --- --- --- --- ---
namespace eval punk::repo {
variable tmpfile_counter 0 ;#additional tmpfile collision avoidance
proc is_fossil {{path {}}} {
if {$path eq {}} { set path [pwd] }
return [expr {[find_fossil $path] ne {}}]
}
proc is_git {{path {}}} {
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] }
scanup $path is_fossil_root
}
proc find_git {{path {}}} {
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] }
#from kettle::path::is.fossil
foreach control {
_FOSSIL_
.fslckout
.fos
} {
set control $path/$control
if {[file exists $control] && [file isfile $control]} {return 1}
}
return 0
}
proc is_git_root {{path {}}} {
if {$path eq {}} { set path [pwd] }
set control [file join $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|lib|scriptapps|*.vfs - and that it's otherwise sensible
proc is_candidate_root {{path {}}} {
if {$path eq {}} { set path [pwd] }
if {[file pathtype $path] eq "relative"} {
if {$::tcl_platform(platform) eq "windows"} {
set normpath [punk::repo::norm [punk::winpath::winpath $path]]
} else {
set normpath [punk::repo::norm $path]
}
} else {
set normpath $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
} {
set req $path/$required
if {(![file exists $req]) || ([file type $req] ne "directory") } {return 0}
}
set src_subs [glob -nocomplain -dir $path/src -types d -tail *]
if {"modules" in $src_subs || "lib" in $src_subs || "scriptapps" in $src_subs} {
return 1
}
foreach sub $src_subs {
if {[string match *.vfs $sub]} {
return 1
}
}
#todo - breadth first search with depth limit (say depth 3?) for *.tm or *.tcl as another positive qualifier for this dir to be a project-root
#we probably don't want to deep search a src folder in case the user is accidentally in some other type of project's tree
#such a src tree could be very large, so if we don't find tcl indicators near the root it's a good bet this isn't a candidate
return 0
}
#keep this message in sync with the programmed requirements of is_candidate_root
#message is not titled - it is intended to be output along with more contextual information from the calling site.
proc is_candidate_root_requirements_msg {} {
set msg ""
append msg "./src directory must exist." \n
append msg "At least one of ./src/lib ./src/modules ./src/scriptapps or a ./src/<something>.vfs folder should exist." \n
#append msg "Alternatively - the presence of any .tm or .tcl files within the top few levels of ./src will suffice." \n
return $msg
}
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 find_roots_and_warnings_dict {path} {
set start_dir $path
#root is a 'project' if it it meets the candidate requrements and is under repo control
#therefore if project is in the closest_types list - candidate will always be there too - and at least one of git or fossil
set root_dict [list closest {} closest_types {} fossil {} git {} candidate {} project {} warnings {}]
set msg ""
#we're only searching in a straight path up the tree looking for a few specific marker files/folder
set fosroot [punk::repo::find_fossil $start_dir]
dict set root_dict fossil $fosroot
set gitroot [punk::repo::find_git $start_dir]
dict set root_dict git $gitroot
set candroot [punk::repo::find_candidate $start_dir]
dict set root_dict candidate $candroot
if {[string length $fosroot]} {
if {([string length $candroot]) && ([string tolower $fosroot] ne [string tolower $candroot])} {
#todo - only warn if this candidate is *within* the found repo root?
append msg "**" \n
append msg "** found folder with /src at or above starting folder - that isn't the fossil root" \n
append msg "** starting folder : $start_dir" \n
append msg "** unexpected : $candroot" \n
append msg "** fossil root : $fosroot ([punk::repo::path_relative $start_dir $fosroot])" \n
append msg "** reporting based on the fossil root found."
append msg "**" \n
}
} else {
if {[string length $gitroot]} {
if {([string length $candroot]) && ([string tolower $gitroot] ne [string tolower $candroot])} {
append msg "**" \n
append msg "** found folder with /src at or above current folder - that isn't the git root" \n
append msg "** starting folder : $start_dir" \n
append msg "** unexpected : $candroot ([punk::repo::path_relative $start_dir $candroot])" \n
append msg "** git root : $gitroot ([punk::repo::path_relative $start_dir $gitroot])" \n
append msg "** reporting based on the git root found."
append msg "**" \n
}
} else {
}
}
if {(![string length [dict get $root_dict fossil]])} {
append msg "Not a punk fossil project" \n
}
#don't warn if not git - unless also not fossil
if {(![string length [dict get $root_dict fossil]]) && (![string length [dict get $root_dict git]])} {
append msg "No repository located at or above starting folder $start_dir" \n
if {![string length [dict get $root_dict candidate]]} {
append msg "No candidate project root found. " \n
append msg "Searched upwards from '$start_dir' expecting a folder with the following requirements: " \n
append msg [punk::repo::is_candidate_root_requirements_msg] \n
} else {
append msg "Candidate project root found at : $candidate" \n
append msg " - consider putting this folder under fossil control (and/or git)" \n
}
}
set pathinfo [list];#exclude not found
foreach repotype [list fossil git candidate] {
set path [dict get $root_dict $repotype]
if {[string length $path]} {
set plen [llength [file split $path]]
lappend pathinfo [list $repotype $path $plen]
}
}
#these root are all inline towards root of drive - so anything of same length should be same path - shorter path must be above another
#we will check equal depth paths are equal strings and raise an error just in case there are problems with the coding for the various path functions used here
#longest path is 'closest' to start_dir
set longest_first [lsort -index 2 $pathinfo]
if {![llength $longest_first]} {
#no repos or candidate - we have already created msg above
} else {
dict set root_dict closest [lindex $longest_first 0 1] ;#the *path* of the closest to start_dir - now we need to find all the types of this len
#see if others same len
set longestlen [lindex $longest_first 0 2]
set equal_longest [lsearch -all -inline -index 2 $longest_first $longestlen]
set ctypes [list]
foreach pinfo $equal_longest {
lappend ctypes [lindex $pinfo 0]
}
dict set root_dict closest_types $ctypes
}
if {[string length [set fosroot [dict get $root_dict fossil]]] && [string length [set gitroot [dict get $root_dict git]]]} {
if {$fosroot ne $gitroot} {
if {[path_a_above_b $fosroot $gitroot]} {
append msg "Found git repo nested within fossil repo - be careful" \n
append msg "** fos root : $fosroot ([punk::repo::path_relative $start_dir $fosroot])" \n
append msg " * git root : $gitroot ([punk::repo::path_relative $start_dir $gitroot])" \n
} else {
append msg "Found fossil repo nested within git repo - be careful" \n
append msg "** git root : $gitroot ([punk::repo::path_relative $start_dir $gitroot])" \n
append msg " * fos root : $fosroot ([punk::repo::path_relative $start_dir $fosroot])" \n
}
}
}
dict set root_dict warnings $msg
#some quick sanity checks..
set ctypes [dict get $root_dict closest_types]
if {"project" in $ctypes} {
if {"candidate" ni $ctypes} {
set errmsg "find_roots_and_warnings_dict logic error: have project but not also classified as candidate (coding error in punk::repo) - inform developer\n"
append errmsg " warnings gathered before error:\n $msg"
error $errmsg
}
if {("git" ni $ctypes) && ("fossil" ni $ctypes)} {
set errmsg "find_roots_and_warnings_dict logic error: have project but not also at least one of 'git', 'fossil' (coding error in punk::repo) - inform developer\n"
append errmsg " warnings gathered before error:\n $msg"
error $errmsg
}
}
set ctype_paths [list]
foreach ctype [dict get $root_dict closest_types] {
lappend ctype_paths [lindex [dict get $root_dict $ctype] 1] ;# type, path, len
}
set unique [lsort -unique $ctype_paths]
if {[llength $unique] > 1} {
# this may be a filesystem path representation issue? case? normalisation?
set errmsg "find_roots_and_warnings_dict logic error: different paths for closest folders found (error in punk::repo) - inform developer\n"
append errmsg " warnings gathered before error:\n $msg"
error $errmsg
}
return $root_dict
}
#------------------------------------
#limit to exec so full punk shell not required in scripts
proc git_revision {{path {}}} {
if {$path eq {}} { set path [pwd] }
# ::kettle::path::revision.git
do_in_path $path {
try {
#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]
}
}
return [string trim $v]
}
proc git_remote {{path {{}}}} {
if {$path eq {}} { set path [pwd] }
do_in_path $path {
try {
#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] -remote -v] ;# consider 'git rev-parse --short HEAD'
} on error {e o} {
set v [lindex [split [dict get $o -errorinfo] \n] 0]
}
}
return [string trim $v]
}
proc fossil_revision {{path {}}} {
if {$path eq {}} { set path [pwd] }
# ::kettle::path::revision.fossil
set fossilcmd [auto_execok fossil]
if {[llength $fossilcmd]} {
do_in_path $path {
set info [::exec {*}$fossilcmd info]
}
return [lindex [grep {checkout:*} $info] 0 1]
} else {
return Unknown
}
}
proc fossil_remote {{path {}}} {
if {$path eq {}} { set path [pwd] }
# ::kettle::path::revision.fossil
set fossilcmd [auto_execok fossil]
if {[llength $fossilcmd]} {
do_in_path $path {
set info [::exec {*}$fossilcmd remote ls]
}
return [string trim $v]
} else {
return Unknown
}
}
#------------------------------------
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} {
if {$path eq {}} { set path [pwd] }
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
set base [file dirname $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
#This needs fixing for general use.. not necessarily just for project repos
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
set here [pwd]
try {
cd $path
uplevel 1 $script
} finally {
cd $here
}
}
proc scanup {path cmd} {
if {$path eq {}} { set path [pwd] }
#based on kettle::path::scanup
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
while {1} {
# Found the proper directory, per the predicate.
if {[{*}$cmd $path]} { return $path }
# Not found, walk to parent
set new [file dirname $path]
# Stop when reaching the root.
if {$new eq $path} { return {} }
if {$new eq {}} { return {} }
# Ok, truly walk up.
set path $new
}
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]
}
proc rgrep {pattern data} {
set data [string map [list \r\n \n] $data]
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]}]
}
#whether path is at and/or below one of the vfs mount points
#The design should facilitate nested vfs mountpoints
proc path_vfs_info {filepath} {
}
#file normalize is expensive so this is too
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]]
}
#MUCH faster version for absolute path prefix (pre-normalized)
proc path_strip_alreadynormalized_prefixdepth {path prefix} {
return [file join \
{*}[lrange \
[file split $path] \
[llength [file split $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
interp alias {} fossil_revision {} ::punk::repo::fossil_revision
interp alias {} is_git {} ::punk::repo::is_git
interp alias {} is_git_root {} ::punk::repo::is_git_root
interp alias {} find_git {} ::punk::repo::find_git
interp alias {} git_revision {} ::punk::repo::git_revision
interp alias {} gs {} git status -sb
interp alias {} gr {} ::punk::repo::git_revision
interp alias {} gl {} git log --oneline --decorate ;#decorate so stdout consistent with what we see on console
interp alias {} glast {} git log -1 HEAD --stat
interp alias {} gconf {} git config --global -l
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::repo [namespace eval punk::repo {
variable version
set version 0.1.0
}]
return

321
src/bootsupport/punk/winpath-0.1.0.tm

@ -0,0 +1,321 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punk::winpath 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::winpath {
namespace export winpath windir cdwin cdwindir illegalname_fix illegalname_test
#review - is this intended to be useful/callable on non-windows platforms?
#it should in theory be useable from another platform that wants to create a path for use on windows.
#In this case - we shouldn't examine what volumes exist (assume A: .. Z: are valid)
#review zipfs:// other uri schemes?
proc winpath {path} {
#NOTE: tcl file exists gives different answers on windows for paths like /c depending on cwd (presumably based on file pathtype of volumerelative)
#This is add odds with attempting to navigate on a windows system which has cygwin, wsl etc... It also makes it difficult for functions intended to operate independent of CWD.
#e.g there is potential confusion when there is a c folder on c: drive (c:/c)
#I will attempt to provide a coherent operation for winpath ./ ../ etc , but it may disallow for example; change to /something or /x where these don't match a driveletter or /mnt
#whereas tcl may allow cd to /something if a something folder happens to exist on the current volume based on cwd.
#I think it's preferable to require an explicit driveletter /x or /mnt when using unix-like paths on windows - but practical considerations may prove me wrong..
#It's possible that this function should also ignore the current set of driveletters - and operate completely independent of whether a path actually exists
#This makes it hard to use things like 'file normalize' - which also looks at things like current volume.
#
#Note for example the results of 'which' grep on windows can produce a path like /c/Users/somewhere/bin/grep
#which tcl's file normalize may change to C:/c/Users or X:/c/Users - based on current volumen. Given that C:/c might exist - this can be problematic in a couple of ways.
#The mixing of unix-like and windows commands on the same machine is a large part of the problem.. but this mix is now common
#
#convert /c/etc to C:/etc
set re_slash_x_slash {^/([[:alpha:]]){1}/.*}
set re_slash_else {^/([[:alpha:]]*)(.*)}
set volumes [file volumes]
#exclude things like //zipfs:/
set driveletters [list]
foreach v $volumes {
if {[regexp {^([[:alpha:]]){1}:/$} $v _ letter]} {
lappend driveletters $letter
}
}
#puts stderr "->$driveletters"
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/([[: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} {
set letter $firstpart
set path [string toupper $letter]:/
} else {
#attempt to use cygpath helper
if {![catch {
set cygpath [runout -n cygpath -w $path] ;#!
set ::punk::last_run_display [list] ;#hack - review shouldn't really be necessary.. but because we call winpath from ./ - the repl looks for last_run_display
} errM]} {
set path [string map [list "\\" "/"] $cygpath]
} else {
error "Path '$path' does not appear to be in a standard form. For unix-like paths on windows such as /x, x must correspond to a drive letter. Consider installing cygwin's cygpath tool to see if that helps."
}
}
}
#puts stderr "=> $path"
#things like 'which' seem to return a path minus the .exe - so we'll just test the containing folder
#
#By now file normalize shouldn't do too many shannanigans related to cwd..
#We want it to look at cwd for relative paths.. but we don't consider things like /c/Users to be relative even on windows
if {![file exists [file dirname $path]]} {
set path [file normalize $path]
#may still not exist.. that's ok.
}
#file normalize may change backslashes to forward slashes.. including things like the special \\?\ prefix which is intended to stop windows api from parsing a name
#2023 - this is ok as //?/ also seems to work.. but it is unclear if that is because Tcl is re-converting to backslashes
if {[illegalname_test $path]} {
set path [illegalname_fix $path]
}
return $path
}
proc windir {path} {
if {$path eq "~"} {
#as the tilde hasn't been normalized.. we can't assume we're running on the actual platform
return ~/..
}
return [file dirname [winpath $path]]
}
#REVIEW high-coupling
proc cdwin {path} {
set path [winpath $path]
if {$::repl::running} {
repl::term::set_console_title $path
}
cd $path
}
proc cdwindir {path} {
set path [winpath $path]
if {$::repl::running} {
repl::term::set_console_title $path
}
cd [file dirname $path]
}
#\\servername\share etc or \\?\UNC\servername\share etc.
proc is_unc_path {path} {
set path [string map [list \\ /] $path] ;#normalize to forward slashes for testing purposes (and forward slashes seem to be auto-translated by windows anyway)
if {[string first "//" $path] == 0} {
#check for "Dos device path" syntax
if {[string range $path 0 3] in [list "//?/" "//./"]} {
#Note that //./ doesn't appear to be supported in Tcl as at 2023-08 - but //?/ works (except for //?/UNC/Server/share)
if {[string range $path 4 6] eq "UNC"} {
return 1
} else {
#some other Dos device path. Could be a drive which is mapped to a UNC path - but the path itself isn't a unc path
return 0
}
} else {
#leading double slash and not dos device path syntax
return 1
}
}
return 0
}
#ordinary \\Servername or \\servername\share or \\servername\share\path (or forward-slash equivalent) with no dos device syntax //?/ //./ etc.
proc is_unc_path_plain {path} {
if {[is_unc_path $path]} {
if {![is_dos_device_path]} {
return 1
} else {
return 0
}
} else {
return 0
}
}
#'file attributes', and therefor this operation, is expensive (on windows at least)
proc pwdshortname {{path {}}} {
if {![string length $path]} {
set path [pwd]
} else {
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
}
return [dict get [file attributes $path] -shortname]
}
#dos device path syntax allows windows api to acces extended-length paths and filenames with illegal path characters such as trailing dots or whitespace
#(can exist on server shares and on NTFS - but standard apps can't access without dos device syntax)
proc is_dos_device_path {path} {
set path [string map [list \\ /] $path] ;#normalize to forward slashes for testing purposes (and forward slashes seem to be auto-translated by windows anyway)
if {[string range $path 0 3] in [list "//?/" "//./"]} {
return 1
} else {
return 0
}
}
proc strip_dos_device_prefix {path} {
#it's unlikely to be valid to strip only //?/ from a //?/UNC path so check for it here and diver to strip that.
#(review.. or raise error because a //?/UNC path isn't *strictly* a UNC path? )
if {[is_unc_path $path]} {
return [strip_unc_path_prefix $path]
}
if {[is_dos_device_path $path]} {
return [string range $path 4 end]
} else {
return $path
}
}
proc strip_unc_path_prefix {path} {
if {[is_unc_path $path]} {
#//?/UNC/server/etc
return [string range $path 7 end]
} elseif {is_unc_path_plain $path} {
#plain unc //server
return [string range $path 2 end]
} else {
return $path
}
}
#we don't validate that path is actually illegal because we don't know the full range of such names.
#The caller can apply this to any path.
#don't test for platform here - needs to be callable from any platform for potential passing to windows
proc illegalname_fix {path} {
#don't add extra dos device path syntax protection-prefix if already done
if {[is_unc_path $path]} {
error "illegalname_fix called on UNC path $path - unable to process"
}
if {[is_dos_device_path $path]} {
#we may have appended
return $path
}
#\\servername\share theoretically maps to: \\?\UNC\servername\share in protected form. https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
#NOTE: 2023-08 on windows 10 at least \\?\UNC\Server\share doesn't work - ie we can't use illegalname_fix on UNC paths such as \\Server\share
#(but mapped drive to same path will work)
#Note that test-path cmdlet in powershell is also flaky with regards to \\?\UNC\Server paths.
#It seems prudent for now to disallow \\?\ protection for UNC paths such as \\server\etc
if {[is_unc_path $path]} {
set err ""
append err "illegalname_fix doesn't currently support UNC paths (non dos device leading double slash or //?/UNC/...)"
append err \n " - because //?/UNC/Servername/share is not supported in Tcl (and only minimally even in powershell) as at 2023. (on windows use mapped drive instead)"
error $err
}
#Note: path could still have leading double slash if it is a Dos device path: e.g. //?/c:/etc
if {[file pathtype $path] eq "absolute"} {
if {$path eq "~"} {
# non-normalized ~ is classified as absolute
# tilde special meaning is a bit of a nuisance.. but as it's the entire path in this case.. presumably it should be kept that way
# leave for caller to interpret it - but it's not an illegal name whether it's interpreted with special meaning or not
# unlikely this fix will be called on a plain tilde anyway
return $path
} else {
set fullpath $path
}
} else {
#set fullpath [file normalize $path] ;#very slow on windows
#set fullpath [pwd]/$path ;#will keep ./ in middle of path - not valid for dos-device paths
if {[string range $path 0 1] eq "./"} {
set path [string range $path 2 end]
}
set fullpath [file join [pwd] $path]
}
#For file I/O, the "\\?\" prefix to a path string tells the Windows APIs to disable all string parsing
# and to send the string that follows it straight to the file system.
set protect "\\\\?\\" ;# value is: \\?\ prefix
set protect2 "//?/" ;#file normalize may do this - it still works
#don't use "//./" - not currently supported in Tcl - seems to work in powershell though.
#choose //?/ as normalized version - since likely 'file normalize' will do it anyway, and experimentall, the windows API accepts both REVIEW
return ${protect2}$fullpath
}
#don't test for platform here - needs to be callable from any platform for potential passing to windows
#we can create files with windows illegal names by using //?/ dos device path syntax - but we need to detect when that is required.
proc illegalname_test {path} {
#first test if already protected - we return false even if the file would be illegal without the protection!
if {[is_dos_device_path $path]} {
return 0
}
#we need to exclude things like path/.. path/.
foreach seg [file split $path] {
if {$seg in [list . ..]} {
#review - what if there is a folder or file that actually has a name such as . or .. ?
#unlikely in normal use - but could done deliberately for bad reasons?
#We are unable to check for it here anyway - as this command is intended for checking the path string - not the actual path on a filesystem.
#
#/./ /../ segments don't require protection - keep checking.
continue
}
#only check for actual space as other whitespace seems to work without being stripped
#trailing tab and trailing \n or \r seem to be creatable in windows with Tcl - map to some glyph
if {[string index $seg end] in [list " " "."]} {
#windows API doesn't handle trailing dots or spaces (silently strips) - even though such files can be created on NTFS systems (or seen via samba etc)
return 1
}
}
#glob chars '* ?' are probably illegal.. but although x*y.txt and x?y.txt don't display properly (* ? replaced with some other glyph)
#- they seem to be readable from cmd and tclsh as is.
# pipe symbol also has glyph substitution and behaves the same e.g a|b.txt
#(at least with encoding system utf-8)
#todo - determine what else constitutes an illegal name according to windows APIs and requires protection with dos device syntax
return 0
}
#----------------------------------------------
#leave the winpath related aliases available on all platforms
interp alias {} cdwin {} punk::winpath::cdwin
interp alias {} cdwindir {} punk::winpath::cdwindir
interp alias {} winpath {} punk::winpath::winpath
interp alias {} windir {} punk::winpath::windir
#----------------------------------------------
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::winpath [namespace eval punk::winpath {
variable version
set version 0.1.0
}]
return

79
src/make.tcl

@ -21,6 +21,81 @@ if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - 'try' command not found - aborting"
exit 1
}
#------------------------------------------------------------------------------
#Module loading from /src/bootsupport or src/*.vfs if script is within a .vfs folder
#------------------------------------------------------------------------------
#If the there is a folder directly under the current directory /src/bootsupport which contains .tm files when the starts
# - then it will attempt to preference these modules
# This allows a source update via 'fossil update' 'git pull' etc to pull in support modules for the make script
set startdir [pwd]
set bootsupport [file join $startdir src bootsupport]
if {[file exists $bootsupport]} {
set original_tm_list [tcl::tm::list]
tcl::tm::remove {*}$original_tm_list
set support_modules [glob -nocomplain -dir $bootsupport -type f -tail *.tm]
set tcl_core_packages [list tcl::zlib zlib tcl::oo TclOO tcl::tommath tcl::zipfs tcl Tcl]
if {[llength $support_modules]} {
#only forget all *unloaded* package names if we are started in a .tm containing folder
foreach pkg [package names] {
if {$pkg in $tcl_core_packages} {
continue
}
if {![llength [package versions $pkg]]} {
#puts stderr "Got no versions for pkg $pkg"
continue
}
if {[catch {package present $pkg}]} {
package forget $pkg
}
}
tcl::tm::add $bootsupport
}
#todo - review usecase
if {[string match "*.vfs/*" [info script]]} {
#src/xxx.vfs/lib/app-punk/repl.tcl
#we assume if calling directly into .vfs that the user would prefer to use src/modules - so go up 4 levels
set modulefolder [file dirname [file dirname [file dirname [file dirname [info script]]]]]/modules
} else {
# .../bin/punkXX.exe look for ../modules (i.e modules folder at same level as bin folder)
set modulefolder [file dirname [file dirname [info nameofexecutable]]]/modules
}
if {[file exists $modulefolder]} {
tcl::tm::add $modulefolder
} else {
puts stderr "Warning unable to find module folder at: $modulefolder"
}
if {[file exists [pwd]/modules]} {
tcl::tm::add [pwd]/modules
}
#package require Thread
#These are strong dependencies
# - the repl requires Threading and punk,shellfilter,shellrun to call and display properly.
# tm list already indexed - need 'package forget' to find modules based on current tcl::tm::list
package forget punk::mix
package require punk::mix
package forget punk::repo
package require punk::repo
#restore module paths
set tm_list_now [tcl::tm::list]
foreach p $original_tm_list {
if {$p ni $tm_list_now} {
tcl::tm::add $p
}
}
#------------------------------------------------------------------------------
}
# ** *** *** *** *** *** *** *** *** *** *** ***
#*temporarily* hijack package command
# ** *** *** *** *** *** *** *** *** *** *** ***
@ -147,15 +222,19 @@ if {$::punkmake::command eq "get-project-info"} {
if {[punk::repo::find_fossil $scriptfolder] eq $projectroot} {
set vc "fossil"
set rev [punk::repo::fossil_revision $scriptfolder]
set rem [punk::repo::fossil_remote $scriptfolder]
} elseif {[punk::repo::find_git $scriptfolder] eq $projectroot} {
set vc "git"
set rev [punk::repo::git_revision $scriptfolder]
set rem [punk::repo::git_remote $scriptfolder]
} else {
set vc " - none found -"
set rev "n/a"
set remotes "n/a"
}
puts stdout "- version control : $vc"
puts stdout "- revision : $rev"
puts stdout "- remote : $rem"
puts stdout "- -- --- --- --- --- --- --- --- --- ---"
exit 0

325
src/modules/punk-0.1.tm

@ -5700,81 +5700,151 @@ namespace eval punk {
#todo - package as punk::navdir
#todo - in thread
#todo - streaming version
proc dirfiles_dict {{glob ""}} {
set dir [pwd]
if {$glob eq ""} {
set glob "*"
proc dirfiles_dict {{searchspec ""}} {
#we don't want to normalize..
#for example if the user supplies ../ we want to see ../result
if {[file pathtype $searchspec] eq "relative"} {
set searchbase [pwd]
set listingfor [file join $searchbase $searchspec]
} else {
set searchbase ""
set listingfor $searchspec
}
set dirname [file dirname $glob] ;# for * or something* will return just "." which is ok
set ftail [file tail $glob]
set ftail [file tail $listingfor]
if {[string first ? $ftail] >= 0 || [string first * $ftail] >=0} {
#has globchar (we only recognise as glob in tail)
set location $dirname
set location [file dirname $listingfor]
set glob $ftail
} else {
set location $dirname/$ftail
set location $listingfor
set glob *
}
#also determine whether vfs. file system x is *much* faster than file attributes
set vfs [list] ;#dict keyed on dir/file name
set dirs [glob -nocomplain -directory $location -type d -tail $glob]
#
set in_vfs 0
foreach mount [vfs::filesystem info] {
if {[punk::repo::path_a_atorbelow_b $location $mount]} {
set in_vfs 1
break
}
}
if {$in_vfs} {
set listing [punk::du::lib::du_dirlisting_tclvfs $location]
} else {
set listing [punk::du::dirlisting $location]
}
#set dirs [glob -nocomplain -directory $location -type d -tail $glob]
set dirs [dict get $listing dirs]
set files [dict get $listing files]
set filesizes [dict get $listing filesizes]
set vfsmounts [dict get $listing vfsmounts]
set flaggedhidden [dict get $listing flaggedhidden]
set nonportable [list] ;#illegal file/folder names for windows e.g trailing dot or trailing space - can still be read if //?/ AND shortname used
set underlayfiles [list]
set underlayfilesizes [list]
if {[llength $vfsmounts]} {
foreach vfsmount $vfsmounts {
if {[set fposn [lsearch $files $vfsmount]] >= 0} {
lappend underlayfiles [lindex $files $fposn]
set files [lreplace $files $fposn $fposn]
if {[llength $filesizes]} {
lappend underlayfilesizes [lindex $filesizes $fposn]
set filesizes [lreplace $filesizes $fposn $fposn]
}
lappend dirs $vfsmount
} elseif {$vfsmount in $dirs} {
#either dirlisting mech was aware of vfs.. or mountpoint is overlaying an underlying folder
#for now - do nothing
#todo - review. way to query dirlisting mech to see if we are hiding a folder?
} else {
#vfs mount but dirlisting mechanism didn't detect as file or folder
lappend dirs $vfsmount
}
}
}
#NOTE: -types {hidden d} * may return . & .. on unix platforms - but will not show them on windows.
#A mounted vfs exe (e.g sometclkit.exe) may be returned by -types {hidden d} on windows - but at the same time has "-hidden 0" in the result of file attr.
set hiddendirs [glob -nocomplain -directory $location -type {hidden d} -tail $glob] ;#non-unix platforms may have attributes to indicate hidden status even if filename doesn't have leading dot.
foreach hidden $hiddendirs {
if {$hidden ni $dirs} {
lappend dirs $hidden
}
#non-unix platforms may have attributes to indicate hidden status even if filename doesn't have leading dot.
#mac & windows have these
#windows doesn't consider dotfiles as hidden - mac does (?)
#we add dotfiles to flaggedhidden list in case there is some other mechanism that has flagged items as hidden
if {$::tcl_platform(platform) ne "windows"} {
lappend flaggedhidden {*}[lsearch -all -inline [concat $dirs $files] ".*"]
set flaggedhidden [lsort -unique $flaggedhidden]
}
set dirs [lsort $dirs] ;#todo - natsort
foreach d $dirs {
if {[lindex [file system $d] 0] eq "tclvfs"} {
lappend vfs $d [file system $d]
}
}
#foreach d $dirs {
# if {[lindex [file system $d] 0] eq "tclvfs"} {
# lappend vfs $d [file system $d]
# }
#}
#glob -types {hidden} will not always return the combination of glob -types {hidden f} && -types {hidden d} (on windows anyway)
set files [glob -nocomplain -directory $location -type f -tail $glob]
set hiddenfiles [glob -nocomplain -directory $location -type {hidden f} -tail $glob]
foreach hidden $hiddenfiles {
if {$hidden ni $files} {
lappend files $hidden
}
}
set files [lsort $files] ;#todo natsort
set illegalwinfiles [list]
foreach fname $files {
if {[punk::winpath::illegalname_test $fname]} {
lappend illegalwinfiles $fname
}
}
set illegalwindirs [list]
foreach dname $dirs {
if {[punk::winpath::illegalname_test $dname]} {
lappend illegalwindirs $dname
foreach nm [concat $dirs $files] {
if {[punk::winpath::illegalname_test $nm]} {
lappend nonportable $nm
}
}
return [list dirs $dirs hiddendirs $hiddendirs vfs $vfs files $files hiddenfiles $hiddenfiles illegalwinfiles $illegalwinfiles illegalwindirs $illegalwindirs location $location]
set updated [dict create dirs $dirs files $files nonportable $nonportable flagedhidden $flaggedhidden underlayfiles $underlayfiles underlayfilesizes $underlayfilesizes location $location searchbase $searchbase]
return [dict merge $listing $updated]
#return [list dirs $dirs vfsmounts $vfsmounts files $files filesizes $filesizes underlayfiles $underlayfiles underlayfilesizes $underlayfilesizes nonportable $nonportable flaggedhidden $flaggedhidden flaggedsystem $flaggedsystem location $location searchbase $searchbase]
}
#todo - color key via repl-telemetry? help command? documentation? or add tag columns as done in namespace listing?
proc dirfiles_dict_as_lines {contents} {
proc dirfiles_dict_as_lines {contents args} {
set defaults [list -stripbase 0]
set opts [dict merge $defaults $args]
set opt_stripbase [dict get $opts -stripbase]
package require overtype
set dirs [dict get $contents dirs]
set hiddendirs [dict get $contents hiddendirs]
set links [dict get $contents links]
set files [dict get $contents files]
set hiddenfiles [dict get $contents hiddenfiles]
set illegalwindirs [dict get $contents illegalwindirs]
set illegalwinfiles [dict get $contents illegalwinfiles]
set vfs [dict get $contents vfs]
set filesizes [dict get $contents filesizes]
set underlayfiles [dict get $contents underlayfiles]
set underlayfilesizes [dict get $contents underlayfilesizes]
set flaggedhidden [dict get $contents flaggedhidden]
set flaggedreadonly [dict get $contents flaggedreadonly]
set flaggedsystem [dict get $contents flaggedsystem]
set nonportable [dict get $contents nonportable] ;# illegal file/folder names from windows perspective
set vfsmounts [dict get $contents vfsmounts]
set searchbase [dict get $contents searchbase]
if {$opt_stripbase} {
set filetails [list]
set dirtails [list]
foreach fileset [list dirs files links underlayfiles flaggedhidden flaggedsystem nonportable vfsmounts] {
set stripped [list]
foreach f [set $fileset] {
lappend stripped [strip_prefix_depth $f $searchbase]
}
set $fileset $stripped
}
}
set widest [pipedata [list {*}$dirs ""] {lmap v $data {string length $v}} {tcl::mathfunc::max {*}$data}]
@ -5786,37 +5856,37 @@ namespace eval punk {
set f1 [a+ white bold]
set f2 [a+]
if {[string length $d]} {
if {$d in $hiddendirs} {
if {$d in $flaggedhidden} {
set d1 [a+ cyan]
}
if {[dict exists $vfs $d]} {
if {$d in $hiddendirs} {
if {$d in $vfsmounts} {
if {$d in $flaggedhidden} {
#we could have a hidden dir which is also a vfs.. color will be overridden giving no indicatio of 'hidden' status - REVIEW
#(This situation encountered on windows - even though file attr showed -hidden 0 - the glob with -types hidden returned it.. possibly a tcl glob bug on windows)
#mark it differently for now.. (todo bug report?)
if {$d in $illegalwindirs} {
if {$d in $nonportable} {
set d1 [a+ red Yellow bold]
} else {
set d1 [a+ green Purple bold]
}
} else {
if {$d in $illegalwindirs} {
if {$d in $nonportable} {
set d1 [a+ red White bold]
} else {
set d1 [a+ green bold]
}
}
} else {
if {$d in $illegalwindirs} {
if {$d in $nonportable} {
set d1 [a+ red bold]
}
}
}
if {[string length $f]} {
if {$f in $hiddenfiles} {
if {$f in $flaggedhidden} {
set f1 [a+ Purple]
} else {
if {$f in $illegalwinfiles} {
if {$f in $nonportable} {
set f1 [a+ red bold]
}
}
@ -5827,11 +5897,20 @@ namespace eval punk {
return [list_as_lines $displaylist]
}
proc dirfiles {{glob ""}} {
set contents [dirfiles_dict $glob]
proc dirlist {{location ""}} {
set contents [dirfiles_dict $location]
return [dirfiles_dict_as_lines $contents -stripbase 1]
}
#dirfiles dirfiles_dict always deliberately return absolute *unnormalized* path
#e.g when cwd is c:/repo/jn/shellspy dirfiles ../../ will return something like:
# c:/repo/jn/shellspy/../../blah
proc dirfiles {{location ""}} {
set contents [dirfiles_dict $location]
return [dirfiles_dict_as_lines $contents]
}
#extra slash implies more verbosity (ie display commands instead of just nschildren)
interp alias {} :/ {} punk::ns/ /
interp alias {} :// {} punk::ns/ //
@ -5922,7 +6001,7 @@ namespace eval punk {
set a1 [lindex $args 0]
set curdir [pwd]
set path1 [path_to_absolute $a1 $curdir $::tcl_platform(platform)]
set fullpath [join $path1 {*}[lrange $args 1 end] /]
set fullpath [file join $path1 {*}[lrange $args 1 end]]
if {[file exists $fullpath]} {
error "Folder $fullpath already exists"
@ -5966,6 +6045,102 @@ namespace eval punk {
}
return $path_absolute
}
proc strip_prefix_depth {path prefix} {
set tail [lrange [file split $path] [llength [file split $prefix]] end]
if {[llength $tail]} {
return [file join {*}$tail]
} else {
return ""
}
}
#todo
proc format_number {number {delim ""} {groupsize ""}} {
if {"windows" eq $::tcl_platform(platform)} {
if {![catch {package require twapi}]} {
if {$delim eq "" && $groupsize eq ""} {
set localeid [twapi::get_system_default_lcid]
return [twapi::format_number $number $localeid -idigits -1]
} else {
if {$delim eq ""} {set delim ","}
if {$groupsize eq ""} {set groupsize 3}
return [twapi::format_number $number 0 -idigits -1 -sthousand $delim -sgrouping $groupsize]
}
}
}
#todo - get configured user defaults
set delim ","
set groupsize 3
return [delimit_number $number $delim $groupsize]
}
#from wiki https://wiki.tcl-lang.org/page/Delimiting+Numberse
# Given a number represented as a string, insert delimiters to break it up for
# readability. Normally, the delimiter will be a comma which will be inserted every
# three digits. However, the delimiter and groupsize are optional arguments,
# permitting use in other locales.
#
# The string is assumed to consist of digits, possibly preceded by spaces,
# and possibly containing a decimal point, i.e.: [:space:]*[:digit:]*\.[:digit:]*
proc delimit_number {number {delim ","} {GroupSize 3}} {
# First, extract right hand part of number, up to and including decimal point
set point [string last "." $number];
if {$point >= 0} {
set PostDecimal [string range $number [expr $point + 1] end];
set PostDecimalP 1;
} else {
set point [expr [string length $number] + 1]
set PostDecimal "";
set PostDecimalP 0;
}
# Now extract any leading spaces.
set ind 0;
while {[string equal [string index $number $ind] \u0020]} {
incr ind;
}
set FirstNonSpace $ind;
set LastSpace [expr $FirstNonSpace - 1];
set LeadingSpaces [string range $number 0 $LastSpace];
# Now extract the non-fractional part of the number, omitting leading spaces.
set MainNumber [string range $number $FirstNonSpace [expr $point -1]];
# Insert commas into the non-fractional part.
set Length [string length $MainNumber];
set Phase [expr $Length % $GroupSize]
set PhaseMinusOne [expr $Phase -1];
set DelimitedMain "";
#First we deal with the extra stuff.
if {$Phase > 0} {
append DelimitedMain [string range $MainNumber 0 $PhaseMinusOne];
}
set FirstInGroup $Phase;
set LastInGroup [expr $FirstInGroup + $GroupSize -1];
while {$LastInGroup < $Length} {
if {$FirstInGroup > 0} {
append DelimitedMain $delim;
}
append DelimitedMain [string range $MainNumber $FirstInGroup $LastInGroup];
incr FirstInGroup $GroupSize
incr LastInGroup $GroupSize
}
# Reassemble the number.
if {$PostDecimalP} {
return [format "%s%s.%s" $LeadingSpaces $DelimitedMain $PostDecimal];
} else {
return [format "%s%s" $LeadingSpaces $DelimitedMain];
}
}
#NOTE - as we expect to run other apps (e.g Tk) in the same process, but possibly different threads - we should be careful about use of cd which is per-process not per-thread.
#As this function recurses and calls cd multiple times - it's not thread-safe.
#Another thread could theoretically cd whilst this is running.
@ -5996,11 +6171,23 @@ namespace eval punk {
set matchinfo [punk::dirfiles_dict]
set dircount [llength [dict get $matchinfo dirs]]
set filecount [llength [dict get $matchinfo files]]
set location [file normalize [dict get $matchinfo location]]
#set location [file normalize [dict get $matchinfo location]]
set location [dict get $matchinfo location]
#result for glob is count of matches - use dirfiles etc for script access to results
set result [list location $location dircount $dircount filecount $filecount]
set filesizes [dict get $matchinfo filesizes]
if {[llength $filesizes]} {
set filesizes [lsearch -all -inline -not $filesizes na]
set filebytes [tcl::mathop::+ {*}$filesizes]
lappend result filebytes [format_number $filebytes]
}
if {$::repl::running} {
set out [punk::dirfiles_dict_as_lines $matchinfo]
set out [punk::dirfiles_dict_as_lines $matchinfo -stripbase 1]
#puts stdout $out
#puts stderr [a+ white]$out[a+]
@ -6031,11 +6218,18 @@ namespace eval punk {
set matchinfo [punk::dirfiles_dict [file tail $path]]
set dircount [llength [dict get $matchinfo dirs]]
set filecount [llength [dict get $matchinfo files]]
set location [file normalize [dict get $matchinfo location]]
#result for glob is count of matches - use dirfiles etc for script access to results
set result [list location $location dircount $dircount filecount $filecount]
set filesizes [dict get $matchinfo filesizes]
if {[llength $filesizes]} {
set filesizes [lsearch -all -inline -not $filesizes na]
set filebytes [tcl::mathop::+ {*}$filesizes]
lappend result filebytes [format_number $filebytes]
}
if {$::repl::running} {
set out [punk::dirfiles_dict_as_lines $matchinfo]
set out [punk::dirfiles_dict_as_lines $matchinfo -stripbase 1]
set chunklist [list]
lappend chunklist [list stdout "[a+ white light]$out[a+]\n"]
lappend chunklist [list result $result]
@ -6079,6 +6273,7 @@ namespace eval punk {
}
}
if {[file type $path] eq "directory"} {
#don't cd to intermediate paths.. could be restricted - yet may have permissions on final path
cd $path
tailcall punk::d/ {*}$atail
}
@ -6099,9 +6294,15 @@ namespace eval punk {
set location [file normalize [dict get $matchinfo location]]
#result for glob is count of matches - use dirfiles etc for script access to results
set result [list location $location dircount $dircount filecount $filecount]
set filesizes [dict get $matchinfo filesizes]
if {[llength $filesizes]} {
set filesizes [lsearch -all -inline -not $filesizes na]
set filebytes [tcl::mathop::+ {*}$filesizes]
lappend result filebytes [format_number $filebytes]
}
if {$::repl::running} {
set out [punk::dirfiles_dict_as_lines $matchinfo]
set out [punk::dirfiles_dict_as_lines $matchinfo -stripbase 1]
#return $out\n[pwd]
set chunklist [list]
lappend chunklist [list stdout "[a+ white light]$out[a+]\n"]
@ -7108,9 +7309,9 @@ namespace eval punk {
interp alias {} d/ {} punk::d/
interp alias {} dd/ {} punk::dd/
interp alias {} dirlist {} punk::dirlist
interp alias {} dirfiles {} punk::dirfiles
interp alias {} dirfiles_dict {} punk::dirfiles_dict
interp alias {} df {} punk::dirfiles
#namespace/command/proc query
interp alias {} nslist {} punk::nslist

618
src/modules/punk/du-999999.0a1.0.tm

@ -18,156 +18,41 @@
## Requirements
##e.g package require frobz
namespace eval punk::du {
variable has_twapi 0
}
if {"windows" eq $::tcl_platform(platform)} {
if {[catch {package require twapi}]} {
puts stderr "Warning: punk::du - unable to load twapi. Disk operations may be much slower on windows without the twapi package"
} else {
set punk::du::has_twapi 1
}
package require punk::winpath
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::du {
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 * .*]
#set files {}
#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]
set filesizes [list]; #not available in listing-call - as opposed to twapi which can do it as it goes
return [list $dirs $files $filesizes]
}
# get listing without using unix-tools (may not be installed on the windows system)
proc du_listing_twapi {folderpath} {
package require punk::winpath
set dirs [list]
set files [list]
set filesizes [list]
try {
if {[string length [file normalize $folderpath]] >= 250} {
set folderpath_shortname [punk::winpath::pwdshortname $folderpath]
set iterator [twapi::find_file_open $folderpath_shortname/* -detail basic]
} else {
set iterator [twapi::find_file_open $folderpath/* -detail basic] ;# -detail full only adds data to the altname field
}
} on error args {
if {[string match "*denied*" $args]} {
#output similar format as unixy du
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args"
return [list {} {} {}]
}
#possibly an illegal windows filename - easily happens on a machine with WSL or with drive mapped to unix share
set errmsg "error reading folder: $folderpath (len:[string length [file normalize $folderpath]])\n"
append errmsg "error: $args" \n
append errmsg "errorcode: $::errorCode" \n
append errmsg {retrying with with windows altname}
puts stderr $errmsg
try {
#go to parent and re-fetch this folder with altnames
#use normalize to get full path - in case we are currently at "."
set parent [file dirname [file normalize $folderpath]]
set iterator [twapi::find_file_open $parent/* -detail full] ;# -detail full because we need altname!
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
if {$nm eq [file tail $folderpath]} {
break
}
}
if {$nm ne [file tail $folderpath]} {
error "failed to re-fetch current directory info with altpath info"
}
set altname [dict get $iteminfo altname]
puts stderr "using altname $parent/$altname"
set iterator [twapi::find_file_open $parent/$altname/* -detail basic]
proc dirlisting {{folderpath {}}} {
if {[lib::pathcharacterlen $folderpath] == 0} {
set folderpath [pwd]
} elseif {[file pathtype $folderpath] ne "absolute"} {
#file normalize relativelly slow - avoid in inner loops
#set folderpath [file normalize $folderpath]
} on error args {
set errmsg "error reading folder: $parent or $folderpath\n"
append errmsg "error: $args"
append errmsg "aborting.."
error $errmsg
}
}
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
if {$nm in {. ..}} {
continue
}
set attrinfo [twapi::decode_file_attributes [dict get $iteminfo attrs]]
if {"reparse_point" in $attrinfo} {
#we will treat as a zero sized file.. review - option -L for symlinks like BSD du?
#Note 'file readlink' can fail on windows - reporting 'invalid argument' - according to tcl docs, 'On systems that don't support symbolic links this option is undefined'
#The link may be viewable ok in windows explorer, and cmd.exe /c dir and unix tools such as ls
#if we need it without resorting to unix-tools that may not be installed: exec {*}[auto_execok dir] /A:L {c:\some\path}
#e.g (stripped of headers/footers and other lines)
#2022-10-02 04:07 AM <SYMLINKD> priv [\\?\c:\repo\elixir\gameportal\apps\test\priv]
#Note we will have to parse beyond header fluff as /B strips the symlink info along with headers.
#du includes the size of the symlink
#but we can't get it with tcl's file size
#twapi doesn't seem to have anything to help read it either (?)
#the above was verified with a symlink that points to a non-existant folder.. mileage may vary for an actually valid link
#
#Note also - a shortcut created in explorer with drag and drop to an existant folder is a different animal to a symlink (file with .lnk extension) even though it looks the same in explorer window.
#This .lnk seems to be an ordinary file and we can treat as such here.
lappend files $nm
lappend filesizes 0
} elseif {"directory" in $attrinfo} {
lappend dirs $nm
} else {
lappend files $nm
lappend filesizes [dict get $iteminfo size]
}
}
return [list $dirs $files $filesizes]
#run whichever of du_dirlisting_twapi, du_dirlisting_generic, du_dirlisting_unix has been activated
set dirinfo [active::du_dirlisting $folderpath]
}
#Note that unix du seems to do depth-first - which makese sense when piping.. as output can be emitted as we go rather than requiring sort at end.
#breadth-first with sort can be quite fast though
#breadth-first with sort can be quite fast .. but memory usage can easily get out of control
proc du { args } {
variable has_twapi
package require struct::set
@ -201,8 +86,13 @@ namespace eval punk::du {
set opts $args
# flags in args are singletons (or longopts --something=somethingelse) or sometimes pairopts
# process any pairopts first
# flags in args are solos (or longopts --something=somethingelse) or sometimes pairopts
# we don't currently support mashopts (ie -xy vs separate -x -y)
#-------------------------------------------------------
# process any pairopts first and remove the pair
# (may also process some solo-opts)
set opt_depth -1
if {[set posn [lsearch $opts -d]] >= 0} {
@ -217,7 +107,9 @@ namespace eval punk::du {
}
}
}
#----
#-------------------------------------------------------
#only solos and longopts remain in the opts now
set lastarg [lindex $opts end]
if {[string length $lastarg] && (![string match -* $lastarg])} {
@ -256,6 +148,10 @@ namespace eval punk::du {
if {"--extra" in $lc_opts} {
set opt_extra 1
}
set opt_vfs 0
if {"--vfs" in $lc_opts} {
set opt_vfs 1
}
@ -263,21 +159,7 @@ namespace eval punk::du {
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 is_windows [expr {$::tcl_platform(platform) eq "windows"}]
set has_twapi 0 ;#default assumption
if {$is_windows} {
if {![catch {
package require twapi
}
]} {
set has_twapi 1
}
}
set zero [expr {0}]
# ## ### ### ### ###
@ -287,11 +169,11 @@ namespace eval punk::du {
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]]
lappend dir_depths_remaining [list $zero $zero [expr {1}] [lib::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]
lappend dir_depths_remaining [list $zero $zero [expr {1}] [lib::du_lit [file tail $dir]] $zero]
set leveldircount 1
}
set level [expr {0}]
@ -324,19 +206,39 @@ namespace eval punk::du {
#return
#}
if {$is_windows && $has_twapi} {
lassign [du_listing_twapi $folderpath] dirs files filesizes; #twapi supports gathering file sizes during directory contents traversal
#twapi supports gathering file sizes during directory contents traversal
#for dirlisting methods that return an empty list in filesizes whilst files has entries - we will need to populate it below
#e.g tcl glob based dirlisting doesn't support gathering file sizes at the same time
set in_vfs 0
if {$opt_vfs} {
foreach vfsmount [vfs::filesystem info] {
if {[punk::repo::path_a_atorbelow_b $folderpath $vfsmount]} {
set in_vfs 1
break
}
}
}
if {$in_vfs} {
set du_info [lib::du_dirlisting_tclvfs $folderpath]
} else {
lassign [du_listing $folderpath] dirs files filesizes ;#filesizes is empty list if listing mechanism doesn't support it (as Tcl glob doesn't)
#run the activated function (proc imported to active namespace and renamed)
set du_info [active::du_dirlisting $folderpath]
}
set dirs [dict get $du_info dirs]
set files [dict get $du_info files]
set filesizes [dict get $du_info filesizes]
incr leveldirs [llength $dirs]
incr levelfiles [llength $files]
#lappend dir_depths_remaining {*}[lmap d $dirs {::list $nextdepth [du_lit $cont/$itm] $d $zero}]
#lappend dir_depths_remaining {*}[lmap d $dirs {::list $nextdepth [lib::du_lit $cont/$itm] $d $zero}]
#folderidx is parent index for new dirs
lappend dir_depths_remaining {*}[_du_new_eachdir $dirs $nextlevel $folderidx]
lappend dir_depths_remaining {*}[lib::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}]
@ -344,9 +246,11 @@ namespace eval punk::du {
if {[llength $files] && ![llength $filesizes]} {
#listing mechanism didn't supply corresponding sizes
foreach filename $files {
incr bytecount [file size "$folderpath/$filename"]
#incr bytecount [file size [file join $folderpath $filename]
incr bytecount [file size $filename]
}
} else {
set filesizes [lsearch -all -inline -not $filesizes[unset filesizes] na] ;#only legal non-number is na
set bytecount [tcl::mathop::+ {*}$filesizes]
}
@ -449,6 +353,392 @@ namespace eval punk::du {
}
# copyright 2002 by The LIGO Laboratory
return $retval
}
namespace eval active {
variable functions [list du_dirlisting ""]
variable functions_known [dict create]
#known functions from lib namespace
dict set functions_known du_dirlisting [list du_dirlisting_twapi du_dirlisting_generic du_dirlisting_unix]
proc show_functions {} {
variable functions
variable functions_known
set msg ""
dict for {callname implementations} $functions_known {
append msg "callname: $callname" \n
foreach imp $implementations {
if {[dict get $functions $callname] eq $imp} {
append msg " $imp (active)" \n
} else {
append msg " $imp" \n
}
}
}
return $msg
}
proc set_active_function {callname implementation} {
variable functions
variable functions_known
if {$callname ni [dict keys $functions_known]} {
error "unknown function callname $callname"
}
if {$implementation ni [dict get $functions_known $callname]} {
error "unknown implementation $implementation for callname $callname"
}
dict set functions $callname $implementation
catch {rename ::punk::du::active::$callname ""}
namespace eval ::punk::du::active [string map [list %imp% $implementation %call% $callname] {
namespace import ::punk::du::lib::%imp%
rename %imp% %call%
}]
return $implementation
}
proc get_active_function {callname} {
variable functions
variable functions_known
if {$callname ni [dict keys $functions_known]} {
error "unknown function callname $callname known functions: [dict keys $functions_known]"
}
return [dict get $functions $callname]
}
#where we import & the appropriate du_listing.. function for the platform
}
namespace eval lib {
variable du_literal
variable winfile_attributes [list 16 directory 32 archive 1024 reparse_point 18 [list directory hidden] 34 [list archive hidden] ]
#caching this is faster than calling twapi api each time.. unknown if twapi is calculating from bitmask - or calling windows api
#we could work out all flags and calculate from bitmask.. but it's not necessarily going to be faster than some simple caching mechanism like this
namespace export du_dirlisting_twapi du_dirlisting_generic du_dirlisting_unix
# get listing without using unix-tools (may not be installed on the windows system)
# this dirlisting is customised for du - so only retrieves dirs,files,filesizes (minimum work needed to perform du function)
proc du_dirlisting_twapi {folderpath} {
set altname "" ;#possible we have to use a different name e.g short windows name or dos-device path //?/
# return it so it can be stored and tried as an alternative for problem paths
try {
set iterator [twapi::find_file_open [file join $folderpath *] -detail basic] ;# -detail full only adds data to the altname field
} on error args {
try {
if {[string match "*denied*" $args]} {
#output similar format as unixy du
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
if {[string match "*TWAPI_WIN32 59*" $::errorCode]} {
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args (possibly blocked by permissions or share config e.g follow symlinks = no on samba)"
puts stderr " (errorcode: $::errorCode)\n"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
if {[set plen [pathcharacterlen $folderpath]] >= 250} {
set errmsg "error reading folder: $folderpath (len:$plen)\n"
append errmsg "error: $args" \n
append errmsg "errorcode: $::errorCode" \n
# re-fetch this folder with altnames
#file normalize - aside from being slow - will have problems with long paths - so this won't work.
#this function should only accept absolute paths
#
#
#Note: using -detail full only helps if the last segment of path has an altname..
#To properly shorten we need to have kept track of altname all the way from the root!
#We can .. for now call Tcl's file attributes to get shortname of the whole path - it is *expensive* e.g 5ms for a long path on local ssd
#### SLOW
set fixedpath [dict get [file attributes $folderpath] -shortname]
#### SLOW
append errmsg "retrying with with windows altname '$fixedpath'"
puts stderr $errmsg
} else {
set errmsg "error reading folder: $folderpath (len:$plen)\n"
append errmsg "error: $args" \n
append errmsg "errorcode: $::errorCode" \n
#possibly an illegal windows filename - easily happens on a machine with WSL or with drive mapped to unix share
#we can use //?/path dos device path - but not with tcl functions
#unfortunately we can't call find_file_open directly on the problem name - we have to call the parent folder and iterate through again..
#this gets problematic as we go deeper unless we rewrite the .. but we can get at least one level further here
set parent [file dirname $folderpath]
set badtail [file tail $folderpath]
set iterator [twapi::find_file_open [file join $parent *] -detail full] ;#retrieve with altnames
set fixedtail ""
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
if {$nm eq $badtail} {
set fixedtail [dict get $iteminfo altname]
break
}
}
if {![string length $fixedtail]} {
puts stderr "[file dirname $folderpath] '[file tail $folderpath]':$args (Unable to retrieve altname to progress further with path - returning no contents for this folder)"
return [list dirs {} vfsmounts {} links {} files {} filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
#twapi as at 2023-08 doesn't seem to support //?/ dos device paths..
#Tcl can test only get as far as testing existence of illegal name by prefixing with //?/ - but can't glob inside it
#we can call file attributes on it - but we get no shortname (but we could get shortname for parent that way)
#so the illegalname_fix doesn't really work here
#set fixedpath [punk::winpath::illegalname_fix $parent $fixedtail]
#this has shortpath for the tail - but it's not the canonical-shortpath because we didn't call it on the $parent part REIEW.
set fixedpath [file join $parent $fixedtail]
append errmsg "retrying with with windows dos device path $fixedpath\n"
puts stderr $errmsg
}
set iterator [twapi::find_file_open $fixedpath/* -detail basic]
} on error args {
set errmsg "error reading folder: $folderpath\n"
append errmsg "error: $args"
append errmsg "aborting.."
error $errmsg
}
}
set dirs [list]
set files [list]
set filesizes [list]
set links [list]
set flaggedhidden [list]
set flaggedsystem [list]
set flaggedreadonly [list]
while {[twapi::find_file_next $iterator iteminfo]} {
set nm [dict get $iteminfo name]
set tail_altname [dict get $iteminfo altname] ;#altname of tail - not whole path
set attrinfo [decode_win_attributes [dict get $iteminfo attrs]]
#puts stderr "$iteminfo"
#puts stderr "$nm -> [dict get $iteminfo attrs] -> $attrinfo"
#attributes applicable to any classification
set fullname [file_join_one $folderpath $nm]
if {"hidden" in $attrinfo} {
lappend flaggedhidden $fullname
}
if {"system" in $attrinfo} {
lappend flaggedsystem $fullname
}
if {"readonly" in $attrinfo} {
lappend flaggedreadonly $fullname
}
#main classification
if {"reparse_point" in $attrinfo} {
#we will treat as zero sized for du purposes.. review - option -L for symlinks like BSD du?
#Note 'file readlink' can fail on windows - reporting 'invalid argument' - according to tcl docs, 'On systems that don't support symbolic links this option is undefined'
#The link may be viewable ok in windows explorer, and cmd.exe /c dir and unix tools such as ls
#if we need it without resorting to unix-tools that may not be installed: exec {*}[auto_execok dir] /A:L {c:\some\path}
#e.g (stripped of headers/footers and other lines)
#2022-10-02 04:07 AM <SYMLINKD> priv [\\?\c:\repo\elixir\gameportal\apps\test\priv]
#Note we will have to parse beyond header fluff as /B strips the symlink info along with headers.
#du includes the size of the symlink
#but we can't get it with tcl's file size
#twapi doesn't seem to have anything to help read it either (?)
#the above was verified with a symlink that points to a non-existant folder.. mileage may vary for an actually valid link
#
#Note also - a shortcut created in explorer with drag and drop to an existant folder is a different animal to a symlink (file with .lnk extension) even though it looks the same in explorer window.
#
#links are techically files too, whether they point to a file/dir or nothing.
lappend links $fullname
} elseif {"directory" in $attrinfo} {
if {$nm in {. ..}} {
continue
}
lappend dirs $fullname
} else {
#review - is anything that isn't a reparse_point or a directory, some sort of 'file' in this context? What about the 'device' attribute? Can that occur in a directory listing of some sort?
lappend files $fullname
lappend filesizes [dict get $iteminfo size]
}
}
twapi::find_file_close $iterator
set vfsmounts [get_vfsmounts_in_folder $folderpath]
#also determine whether vfs. file system x is *much* faster than file attributes
#whether or not there is a corresponding file/dir add any applicable mountpoints for the containing folder
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes $filesizes flaggedhidden $flaggedhidden flaggedsystem $flaggedsystem flaggedreadonly $flaggedreadonly altname $altname]
}
proc get_vfsmounts_in_folder {folderpath} {
set vfsmounts [list]
set known_vfs_mounts [vfs::filesystem info]
foreach mount $known_vfs_mounts {
if {[punk::repo::path_a_above_b $folderpath $mount]} {
if {([llength [file split $mount]] - [llength [file split $folderpath]]) == 1} {
#the mount is in this folder
lappend vfsmounts $mount
}
}
}
return $vfsmounts
}
#work around the horrible tilde-expansion thing (not needed for tcl 9+)
proc file_join_one {base newtail} {
if {[string index $newtail 0] ne {~}} {
return [file join $base $newtail]
}
return [file join $base ./$newtail]
}
#this is the cross-platform pure-tcl version - which calls glob multiple times to make sure it gets everythign it needs and can ignore everything it needs to.
#These repeated calls to glob will be a killer for performance - especially on a network share or when walking a large directory structure
proc du_dirlisting_generic {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} * .*]
#set hdirs {}
set dirs [glob -nocomplain -dir $folderpath -types d * .*]
set hlinks [glob -nocomplain -dir $folderpath -types {hidden l} * .*]
#set hlinks {}
set links [glob -nocomplain -dir $folderpath -types l * .*] ;#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} * .*]
#set hfiles {}
set files [glob -nocomplain -dir $folderpath -types f * .*]
#set files {}
#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 [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference [concat $hfiles $files[unset files]] $links]
set links [lsort -unique [concat $links $hlinks]]
if {"windows" eq $::tcl_platform(platform)} {
set flaggedhidden [concat $hdirs $hfiles $hlinks]
} else {
#unix dotted files/folders are not 'flagged' as such - it's a convention - so the client can use the same convention to decide if something is hidden
#this allows us to return less data - but more importantly - reserve flaggedhidden for systems where such a flag exists and avoid conflating the different concepts of what is hidden
set flaggedhidden {}
}
set vfsmounts [get_vfsmounts_in_folder $folderpath]
set filesizes [list]; #not available in listing-call - as opposed to twapi which can do it as it goes
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
#we can halve the number of round trips on unix-like systems, where 'hidden' always corresponds to dotted files
proc du_dirlisting_unix {folderpath} {
set dirs [glob -nocomplain -dir $folderpath -types d * .*] ;# also returns links to dirs
set links [glob -nocomplain -dir $folderpath -types l * .*] ;# links may have dupes - we don't care. struct::set difference will remove
set files [glob -nocomplain -dir $folderpath -types f * .*] ;# also returns links to files
#remove any links from our dirs and files collections
set dirs [struct::set difference $dirs[unset dirs] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference $files[unset files] $links]
set vfsmounts [get_vfsmounts_in_folder $folderpath]
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
proc du_dirlisting_tclvfs {folderpath} {
set dirs [glob -nocomplain -dir $folderpath -types d * .*] ;# also returns links to dirs
#review - how are links handled in vfs? presumably if the vfs is a full implementation they should work at least within the vfs?
set links [glob -nocomplain -dir $folderpath -types l * .*] ;# links may have dupes - we don't care. struct::set difference will remove
set files [glob -nocomplain -dir $folderpath -types f * .*] ;# also returns links to files
#remove any links from our dirs and files collections
set dirs [struct::set difference $dirs[unset dirs] [concat $links [list [file join $folderpath .] [file join $folderpath ..] ]]]
set files [struct::set difference $files[unset files] $links]
#nested vfs mount.. REVIEW - does anything need special handling?
set vfsmounts [get_vfsmounts_in_folder $folderpath]
return [list dirs $dirs vfsmounts $vfsmounts links $links files $files filesizes {} flaggedhidden {} flaggedsystem {} flaggedreadonly {} altname {}]
}
proc decode_win_attributes {bitmask} {
variable winfile_attributes
if {[dict exists $winfile_attributes $bitmask]} {
return [dict get $winfile_attributes $bitmask]
} else {
#list/dict shimmering?
return [lindex [lappend winfile_attributes $bitmask [twapi::decode_file_attributes $bitmask]] end]
}
}
proc du_lit value {
variable du_literal
if {![info exists du_literal($value)]} {
set du_literal($value) $value
}
return $du_literal($value)
}
#v1
proc du_new_eachdirtail {dirtails depth parentfolderidx} {
set newlist {}
upvar folders folders
set parentpath [lindex $folders $parentfolderidx]
set newindex [llength $folders]
foreach dt $dirtails {
lappend folders [file join $parentpath [du_lit $dt]]; #store as a 'path' rather than a string (see tcl::unsupported::representation)
lappend newlist [::list $depth $parentfolderidx $newindex [du_lit $dt] [expr {0}]]
incr newindex
}
return $newlist
}
proc du_new_eachdir {dirpaths depth parentfolderidx} {
set newlist {}
upvar folders folders
set newindex [llength $folders]
foreach dp $dirpaths {
lappend folders $dp
#puts stdout "--->$dp"
lappend newlist [::list $depth $parentfolderidx $newindex [du_lit [file tail $dp]] [expr {0}]]
incr newindex
}
return $newlist
}
#just an experiment
#get length of path which has internal rep of path - maintaining path/list rep without shimmering to string representation.
proc pathcharacterlen {pathrep} {
set l 0
set parts [file split $pathrep]
if {[llength $parts] < 2} {
return [string length [lindex $parts 0]]
}
foreach seg $parts {
incr l [string length $seg]
}
return [expr {$l + [llength $parts] -2}]
}
#slower - doesn't work for short paths like c:/
proc pathcharacterlen2 {pathrep} {
return [tcl::mathop::+ {*}[lmap v [set plist [file split $pathrep]] {[string length $v]}] [llength $plist] -2]
}
#Strip using lengths without examining path components
#without normalization is much faster
proc path_strip_alreadynormalized_prefixdepth {path prefix} {
set tail [lrange [file split $path] [llength [file split $prefix]] end]
if {[llength $tail]} {
return [file join {*}$tail]
} else {
return ""
}
}
}
package require natsort
#interp alias {} du {} .=args>* punk::du |> .=>1 natsort::sort -cols 1 |> list_as_lines <args|
@ -464,8 +754,22 @@ namespace eval punk::du {
}
namespace eval ::punk::du::active {
variable functions
variable functions_kown
if {"windows" eq $::tcl_platform(platform)} {
if {$punk::du::has_twapi} {
set_active_function du_dirlisting du_dirlisting_twapi
} else {
set_active_function du_dirlisting du_dirlisting_generic
}
} else {
set_active_function du_dirlisting du_dirlisting_unix
}
}

196
src/modules/punk/mix-0.2.tm

@ -286,6 +286,193 @@ namespace eval punk::mix::cli {
}
interp alias {} ::punk::mix::cli::newproject {} ::punk::mix::cli::new
proc visible_lib_glob {glob} {
if {[catch {package require natsort}]} {
set has_natsort 0
} else {
set has_natsort 1
}
set matches [lsearch -all -inline [package names] $glob]
set matchinfo [list]
foreach m $matches {
set versions [package versions $m]
if {$has_natsort} {
set versions [natsort::sort $versions]
} else {
set versions [lsort $versions]
}
lappend matchinfo [list $m $versions]
}
return [join [lsort $matchinfo] \n]
}
proc visible_lib_copy_to_modulefolder {library modulefoldername args} {
set defaults [list -askme 1]
set opts [dict merge $defaults $args]
set opt_askme [dict get $opts -askme]
if {[catch {package require natsort}]} {
set has_natsort 0
} else {
set has_natsort 1
}
if {[file pathtype $modulefoldername] eq "absolute"} {
if {![file exists $modulefoldername]} {
error "Path '$modulefoldername' not found. Enter a fully qualified path, or just the tail such as 'modules' if you are within the project to use <projectdir>/src/modules"
}
#use the target folder as the source of projectdir info
set pathinfo [punk::repo::find_roots_and_warnings_dict $modulefoldername]
set projectdir [dict get $pathinfo closest]
set modulefolder_path $modulefoldername
} else {
#use the current working directory as the source of projectdir info
set pathinfo [punk::repo::find_roots_and_warnings_dict [pwd]]
set projectdir [dict get $pathinfo closest]
set modulefolders [lib::find_source_module_paths $projectdir]
foreach k [list modules bootsupport vendormodules] {
set knownfolder [file join $projectdir src $k]
if {$knownfolder ni $modulefolders} {
lappend modulefolders $knownfolder
}
}
set mtails [list]
foreach path $modulefolders {
lappend mtails [file tail $path]
}
if {$modulefoldername ni $mtails} {
set msg "Suplied modulefoldername '$modulefoldername' doesn't appear to be a known module folder within the project at $projectdir\n"
append msg "Known module folders: [lsort $mtails]\n"
append msg "Use a name from the above list, or a fully qualified path\n"
error $msg
}
set modulefolder_path [file join $projectdir src $modulefoldername]
}
puts stdout "-----------------------------"
puts stdout "Using projectdir: $projectdir for visible_lib_copy_to_modulefolder"
puts stdout "-----------------------------"
set libfound [lsearch -all -inline [package names] $library]
if {[llength $libfound] != 1 || ![string length $libfound]} {
error "Library must match exactly one entry in the list of package names visible to the current interpretor: found '$libfound'"
}
set versions [package versions [lindex $libfound 0]]
if {$has_natsort} {
set versions [natsort::sort $versions]
} else {
set versions [lsort $versions]
}
if {![llength $versions]} {
error "No version numbers found for library/module $libfound - sorry, you will need to copy it across manually"
}
puts stdout "Versions of $libfound found: $versions"
set alphaposn [lsearch $versions "999999.*"]
if {$alphaposn >= 0} {
set alpha [lindex $versions $alphaposn]
#remove and tack onto beginning..
set versions [lreplace $versions $alphaposn $alphaposn]
set versions [list $alpha {*}$versions]
}
set ver [lindex $versions end] ;# todo - make selectable! don't assume tail is latest?.. package vcompare?
if {[llength $versions] > 1} {
puts stdout "Version selected: $ver"
}
set loadinfo [package ifneeded $libfound $ver]
if {[llength $loadinfo] == 2 && [lindex $loadinfo 0] eq "source"} {
set source_file [lindex $loadinfo 1]
} elseif {[string match "*source*" $loadinfo]} {
set parts [split $loadinfo ";"]
set sources_found [list]
foreach p $parts {
if {[lindex $p 0] eq "source"} {
#may have args.. e.g -encoding utf-8
lappend sources_found [lindex $p end]
}
}
if {[llength $sources_found] != 1} {
puts stderr "sorry - unable to interpreet source library location"
puts stderr "Only 1 source supported for now: received $loadinfo"
return false
}
set source_file [lindex $sources_found 0]
} else {
puts stderr "sorry - unable to interpret source library location"
puts stderr "Found info: $loadinfo"
return false
}
if {![file exists $source_file]} {
error "Unable to verify source file existence at: $source_file"
}
set source_data [fcat $source_file -translation binary]
if {![string match "*package provide*" $source_data] || ![string match "*$libfound*" $source_data]} {
puts stderr "Sorry - unable to verify source file contains 'package provide' and '$libfound' - copy manually"
return false
}
set moduleprefix [punk::nsprefix $libfound]
if {[string length $moduleprefix]} {
set moduleprefix_parts [punk::nsparts $moduleprefix]
set relative_path [file join {*}$moduleprefix_parts]
} else {
set relative_path ""
}
set pkgtail [punk::nstail $libfound]
set target_path [file join $modulefolder_path $relative_path ${pkgtail}-${ver}.tm]
if {$opt_askme} {
puts stdout "WARNING - you should check that there aren't extra required files for the library/modules"
puts stdout ""
puts stdout "This is not intended for binary modules - use at own risk and check results"
puts stdout ""
puts stdout "Base module path: $modulefolder_path"
puts stdout "Target path : $target_path"
puts stdout "Proceed to create ${pkgtail}-${ver}.tm module? Y|N"
set stdin_state [fconfigure stdin]
fconfigure stdin -blocking 1
set answer [string tolower [gets stdin]]
fconfigure stdin -blocking [dict get $stdin_state -blocking]
if {$answer ne "y"} {
puts stderr "mix visible_lib_copy_to_modulefolder aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts."
return
}
}
if {![file exists $modulefolder_path]} {
puts stdout "Creating module base folder at $modulefolder_path"
file mkdir $modulefolder_path
}
if {![file exists [file dirname $target_path]]} {
puts stdout "Creating relative folder at [file dirname $target_path]"
file mkdir [file dirname $target_path]
}
if {[file exists $target_path]} {
puts stdout "WARNING - module already exists at $target_path"
if {$opt_askme} {
puts stdout "Copy anyway? Y|N"
set stdin_state [fconfigure stdin]
fconfigure stdin -blocking 1
set answer [string tolower [gets stdin]]
fconfigure stdin -blocking [dict get $stdin_state -blocking]
if {$answer ne "y"} {
puts stderr "mix visible_lib_copy_to_modulefolder aborting due to user response '$answer' (required Y|y to proceed) use -askme 0 to avoid prompts."
return
}
}
}
file copy -force $source_file $target_path
return $target_path
}
proc wrap_in_multishell {filepath args} {
set defaults [list -askme 1]
set opts [dict merge $defaults $args]
@ -395,6 +582,7 @@ namespace eval punk::mix::cli {
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"
fconfigure stdin -blocking 1
set answer [string tolower [gets stdin]]
fconfigure stdin -blocking [dict get $stdin_state -blocking]
if {$answer ne "y"} {
@ -597,14 +785,14 @@ 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)
#find src/something folders which are not certain known folders with other purposes, (such as: bootsupport .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 src_subs [glob -nocomplain -dir [file join $candidate src] -type d -tail *]
set antipatterns [list *.vfs vendor* lib _build doc embedded runtime bootsupport]
set tm_folders [list]
foreach sub $src_subs {
foreach anti $antipatterns {
@ -612,7 +800,7 @@ namespace eval punk::mix::cli {
continue
}
}
set testfolder $candidate/src/$sub
set testfolder [file join $candidate src $sub]
set tmfiles [glob -nocomplain -dir $testfolder -type f -tail *.tm]
if {[llength $tmfiles]} {
lappend tm_folders $testfolder

24
src/modules/punk/mix/templates/layouts/project/src/bootsupport/README.md

@ -0,0 +1,24 @@
This is primarily for tcl .tm modules required for your bootstrapping/make/build process.
It could include other files necessary for this process.
The .tm modules here may be required for your build script if it intended the installation operator uses an existing tclsh or other shell as opposed to a tclkit you may have for distribution which is more likely to include necessary libraries.
The modules here are loaded by your initialisation scripts and so can be a snapshot of different versions than those in your project src.
The modules can be your own, or 3rd party such as individual items from tcllib.
You can copy modules from a running punk shell to this location using the pmix command.
e.g
>pmix visible_lib_copy_to_modulefolder some::module::lib bootsupport
The pmix command will help you pick the latest version, and will create any necessary file structure matching the namespace of the package.
e.g the result might be a file such as
<projectname>/src/bootsupport/some/module/lib-0.1.tm
The originating library may not yet be in .tm form.
You can copy a pkgIndex.tcl based library that is composed of a single .tcl file the same way using the above process and it will automatically name and file it appropriately but you need to check that the library doesn't require/load additional files - and that it is Tcl script only.
Always verify that the library is copyable in this manner and test in a shell with tcl::tm::path pointed to ./bootsupport that it works.

21
src/modules/punk/repl-0.1.tm

@ -1,7 +1,3 @@
package provide punk::repl [namespace eval punk::repl {
variable version
set version 0.1
}]
#punk linerepl
#todo - make repls configurable/pluggable packages
@ -826,6 +822,7 @@ proc repl::rputs {args} {
if {[::tcl::mathop::<= 1 [llength $args] 3]} {
set out [lindex $args end]
append out ""; #copy on write
if {([llength $args] > 1) && [lindex $args 0] ne "-nonewline"} {
set this_tail \n
set rputschan [lindex $args 0]
@ -1051,7 +1048,7 @@ proc repl::repl_handler {inputchan prompt_config} {
#===============================================================================
if {[string equal -length [string length "repl_runraw "] "repl_runraw " $commandstr]} {
#pass unevaluated command to runraw
set status [catch {uplevel #0 [list runraw $commandstr]} result]
set status [catch {uplevel #0 [list runraw $commandstr]} raw_result]
} else {
#puts stderr "repl uplevel 0 '$command'"
set status [catch {
@ -1071,8 +1068,10 @@ proc repl::repl_handler {inputchan prompt_config} {
} else {
uplevel 1 {namespace inscope $punk::ns_current $run_command_string}
}
} result]
} raw_result]
}
set result $raw_result
append result ""; #copy on write
#===============================================================================
flush stdout
flush stderr
@ -1311,6 +1310,16 @@ proc repl::repl_handler {inputchan prompt_config} {
}
}
package provide punk::repl [namespace eval punk::repl {
variable version
set version 0.1
}]
package provide punk::repl [namespace eval punk::repl {
variable version
set version 0.1
}]
#repl::start stdin
#exit 0

119
src/modules/punk/repo-999999.0a1.0.tm

@ -27,10 +27,33 @@
#
# path/repo functions
#
package require punk::winpath
if {$::tcl_platform(platform) eq "windows"} {
package require punk::winpath
} else {
catch {package require punk::winpath}
}
package require cksum ;#tcllib
package require fileutil; #tcllib
# -- --- --- --- --- --- --- --- --- --- ---
# For performance/efficiency reasons - use file functions on paths in preference to string operations
# e.g use file join
# branch to avoid unnecessary calls to 'pwd' or 'file normalize' - which can be surprisingly expensive operations (as at tcl 8.7 2023)
# pwd is only expensive if we treat it as a string instead of a list/path
# e.g
# > time {set x [pwd]}
# 5 microsoeconds.. no problem
# > time {set x [pwd]}
# 4 microsoeconds.. still no problem
# > string length $x
# 45
# > time {set x [pwd]}
# 1372 microseconds per iteration ;#!! values above 0.5ms common.. and that's a potential problem in loops that trawl filesystem
# The same sorts of timings occur with file normalize
# also.. even if we build up a path with file join from a base value that has already been normalized - the subsequent normalize will be expensive
# -- --- --- --- --- --- --- --- --- --- ---
namespace eval punk::repo {
variable tmpfile_counter 0 ;#additional tmpfile collision avoidance
@ -112,7 +135,7 @@ namespace eval punk::repo {
}
proc is_git_root {{path {}}} {
if {$path eq {}} { set path [pwd] }
set control $path/.git
set control [file join $path .git]
expr {[file exists $control] && [file isdirectory $control]}
}
proc is_repo_root {{path {}}} {
@ -122,11 +145,14 @@ namespace eval punk::repo {
#require a minimum of /src and /modules|lib|scriptapps|*.vfs - 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]]
if {[file pathtype $path] eq "relative"} {
if {$::tcl_platform(platform) eq "windows"} {
set normpath [punk::repo::norm [punk::winpath::winpath $path]]
} else {
set normpath [punk::repo::norm $path]
}
} else {
set normpath [punk::repo::norm $path]
set normpath $path
}
set unwise_paths [list "/" "/usr/local" "/usr/local/bin" "/usr/local/lib" "c:/windows"]
if {[string tolower $normpath] in $unwise_paths} {
@ -243,9 +269,9 @@ namespace eval punk::repo {
if {(![string length [dict get $root_dict fossil]]) && (![string length [dict get $root_dict git]])} {
append msg "No repository located at or above starting folder $start_dir" \n
if {![string length [dict get $root_dict candidate]]} {
append msg "No candidate project root found. "
append msg "No candidate project root found. " \n
append msg "Searched upwards from '$start_dir' expecting a folder with the following requirements: " \n
append msg [punk::repo::is_candidate_root_requirements_msg]
append msg [punk::repo::is_candidate_root_requirements_msg] \n
} else {
append msg "Candidate project root found at : $candidate" \n
append msg " - consider putting this folder under fossil control (and/or git)" \n
@ -281,7 +307,18 @@ namespace eval punk::repo {
}
if {[string length [set fosroot [dict get $root_dict fossil]]] && [string length [set gitroot [dict get $root_dict git]]]} {
if {$fosroot ne $gitroot} {
if {[path_a_above_b $fosroot $gitroot]} {
append msg "Found git repo nested within fossil repo - be careful" \n
append msg "** fos root : $fosroot ([punk::repo::path_relative $start_dir $fosroot])" \n
append msg " * git root : $gitroot ([punk::repo::path_relative $start_dir $gitroot])" \n
} else {
append msg "Found fossil repo nested within git repo - be careful" \n
append msg "** git root : $gitroot ([punk::repo::path_relative $start_dir $gitroot])" \n
append msg " * fos root : $fosroot ([punk::repo::path_relative $start_dir $fosroot])" \n
}
}
}
@ -314,6 +351,9 @@ namespace eval punk::repo {
return $root_dict
}
#------------------------------------
#limit to exec so full punk shell not required in scripts
proc git_revision {{path {}}} {
if {$path eq {}} { set path [pwd] }
# ::kettle::path::revision.git
@ -328,6 +368,19 @@ namespace eval punk::repo {
}
return [string trim $v]
}
proc git_remote {{path {{}}}} {
if {$path eq {}} { set path [pwd] }
do_in_path $path {
try {
#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] -remote -v] ;# consider 'git rev-parse --short HEAD'
} on error {e o} {
set v [lindex [split [dict get $o -errorinfo] \n] 0]
}
}
return [string trim $v]
}
proc fossil_revision {{path {}}} {
if {$path eq {}} { set path [pwd] }
@ -343,6 +396,20 @@ namespace eval punk::repo {
}
}
proc fossil_remote {{path {}}} {
if {$path eq {}} { set path [pwd] }
# ::kettle::path::revision.fossil
set fossilcmd [auto_execok fossil]
if {[llength $fossilcmd]} {
do_in_path $path {
set info [::exec {*}$fossilcmd remote ls]
}
return [string trim $v]
} else {
return Unknown
}
}
#------------------------------------
proc cksum_path_content {path args} {
dict set args -cksum_content 1
@ -354,7 +421,11 @@ namespace eval punk::repo {
#-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]]
if {$path eq {}} { set path [pwd] }
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
set base [file dirname $path]
set startdir [pwd]
set defaults [list -cksum_content 1 -cksum_meta 1 -cksum_acls 0 -use_tar 1]
@ -391,6 +462,7 @@ namespace eval punk::repo {
if {$path eq $base} {
#attempting to cksum at root/volume level of a filesystem.. extra work
#This needs fixing for general use.. not necessarily just for project repos
puts stderr "cksum_path doesn't yet support cksum of entire volume. (todo)"
return [list error unsupported opts $opts]
}
@ -442,7 +514,9 @@ namespace eval punk::repo {
proc scanup {path cmd} {
if {$path eq {}} { set path [pwd] }
#based on kettle::path::scanup
set path [file normalize $path]
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
while {1} {
# Found the proper directory, per the predicate.
if {[{*}$cmd $path]} { return $path }
@ -489,11 +563,11 @@ namespace eval punk::repo {
set problems {}
foreach tmp {TMPDIR TEMP TMP} {
if { [info exists env($tmp)] } {
lappend attempdirs $env($tmp)
} else {
lappend problems "No environment variable $tmp"
}
if { [info exists env($tmp)] } {
lappend attempdirs $env($tmp)
} else {
lappend problems "No environment variable $tmp"
}
}
switch $tcl_platform(platform) {
@ -622,7 +696,13 @@ namespace eval punk::repo {
return [expr {[path_a_aboveorat_b $path_a $path_b] || [path_a_below_b $path_a $path_b]}]
}
#whether path is at and/or below one of the vfs mount points
#The design should facilitate nested vfs mountpoints
proc path_vfs_info {filepath} {
}
#file normalize is expensive so this is too
proc norm {path {platform env}} {
#kettle::path::norm
#see also wiki
@ -642,6 +722,7 @@ namespace eval punk::repo {
#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 \
@ -649,6 +730,14 @@ namespace eval punk::repo {
[llength [file split [norm $prefix]]] \
end]]
}
#MUCH faster version for absolute path prefix (pre-normalized)
proc path_strip_alreadynormalized_prefixdepth {path prefix} {
return [file join \
{*}[lrange \
[file split $path] \
[llength [file split $prefix]] \
end]]
}
proc fcat {args} {
if {$::tcl_platform(platform) ne "windows"} {

44
src/modules/punk/winpath-999999.0a1.0.tm

@ -156,11 +156,16 @@ namespace eval punk::winpath {
}
}
#'file attributes', and therefor this operation, is expensive (on windows at least)
proc pwdshortname {{path {}}} {
if {![string length $path]} {
set path [pwd]
} else {
if {[file pathtype $path] eq "relative"} {
set path [file normalize $path]
}
}
return [dict get [file attributes [file normalize $path]] -shortname]
return [dict get [file attributes $path] -shortname]
}
#dos device path syntax allows windows api to acces extended-length paths and filenames with illegal path characters such as trailing dots or whitespace
#(can exist on server shares and on NTFS - but standard apps can't access without dos device syntax)
@ -172,12 +177,39 @@ namespace eval punk::winpath {
return 0
}
}
proc strip_dos_device_prefix {path} {
#it's unlikely to be valid to strip only //?/ from a //?/UNC path so check for it here and diver to strip that.
#(review.. or raise error because a //?/UNC path isn't *strictly* a UNC path? )
if {[is_unc_path $path]} {
return [strip_unc_path_prefix $path]
}
if {[is_dos_device_path $path]} {
return [string range $path 4 end]
} else {
return $path
}
}
proc strip_unc_path_prefix {path} {
if {[is_unc_path $path]} {
#//?/UNC/server/etc
return [string range $path 7 end]
} elseif {is_unc_path_plain $path} {
#plain unc //server
return [string range $path 2 end]
} else {
return $path
}
}
#we don't validate that path is actually illegal because we don't know the full range of such names.
#The caller can apply this to any path.
#don't test for platform here - needs to be callable from any platform for potential passing to windows
proc illegalname_fix {path} {
#don't add extra dos device path syntax protection-prefix if already done
if {[is_unc_path $path]} {
error "illegalname_fix called on UNC path $path - unable to process"
}
if {[is_dos_device_path $path]} {
#we may have appended
return $path
}
@ -197,12 +229,20 @@ namespace eval punk::winpath {
if {[file pathtype $path] eq "absolute"} {
if {$path eq "~"} {
# non-normalized ~ is classified as absolute
# tilde special meaning is a bit of a nuisance.. but as it's the entire path in this case.. presumably it should be kept that way
# leave for caller to interpret it - but it's not an illegal name whether it's interpreted with special meaning or not
# unlikely this fix will be called on a plain tilde anyway
return $path
} else {
set fullpath $path
}
} else {
set fullpath [pwd]/$path
#set fullpath [file normalize $path] ;#very slow on windows
#set fullpath [pwd]/$path ;#will keep ./ in middle of path - not valid for dos-device paths
if {[string range $path 0 1] eq "./"} {
set path [string range $path 2 end]
}
set fullpath [file join [pwd] $path]
}
#For file I/O, the "\\?\" prefix to a path string tells the Windows APIs to disable all string parsing
# and to send the string that follows it straight to the file system.

Loading…
Cancel
Save