Julian Noble
1 year ago
20 changed files with 8475 additions and 249 deletions
@ -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. |
||||
|
||||
|
@ -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: |
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
File diff suppressed because it is too large
Load Diff
@ -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 |
@ -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 |
@ -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. |
||||
|
||||
|
Loading…
Reference in new issue