make.tcl modules + bootsupport + vfscommonupdate after the punk::path ***
source changes: path-0.3.0.tm -> path-0.4.0.tm snapshots in
src/bootsupport/modules and src/vfs/_vfscommon.vfs. The runtests parent
resolves punk::path from bootsupport - the refreshed snapshot is what enables
*** targeting at the runner discovery layer (discovery.tcl requires 0.4.0-).
Known ride-along not included: the bootsupport templates-0.2.0.tm modpod copy
failed to refresh ('invalid argument' - destination held open, likely a
running shell's zipfs mount; the stale copy predates this session) - reported
separately.
Claude-Session: https://claude.ai/code/session_01BNUVVkYq9vHa6G3S9a3XTZ
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
#[titledesc {Filesystem path utilities}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk path filesystem utils}] [comment {-- Description at end of page heading --}]
@ -725,9 +725,10 @@ namespace eval punk::path {
proc pathglob_as_re {pathglob} {
#*** !doctools
#[call [fun pathglob_as_re] [arg pathglob]]
#[para] Returns a regular expression for matching a path to a glob pattern which can contain glob chars *|? in any segment of the path structure
#[para] Returns a regular expression for matching a path to a glob pattern which can contain glob chars *|**|***|? in any segment of the path structure
#[para] Does not support square bracket globs or character classes.
#[para] ** matches any number of subdirectories.
#[para] ** as a whole segment matches one or more segments.
#[para] *** as a whole segment matches zero or more segments - e.g /etc/*** will match /etc itself as well as anything below it. (added 2026-07-20, G-093)
#[para] e.g /etc/**/*.txt will match any .txt files at any depth below /etc (except directly within /etc itself)
#[para] e.g /etc/**.txt will match any .txt files at any depth below /etc
#[para] any segment that does not contain ** must match exactly one segment in the path
@ -738,6 +739,7 @@ namespace eval punk::path {
#todo - consider whether a way to escape the glob chars ? * is practical - to allow literals ? *
# - would require counting immediately-preceding backslashes
set zom "\x00zom\x00" ;#zero-or-more-segments marker for whole-segment *** (segment regexes never contain \x00)
set pats [list]
foreach seg [file split $pathglob] {
if {[string range $seg end end] eq "/"} {
@ -746,6 +748,12 @@ namespace eval punk::path {
switch -- $seg {
* {lappend pats {[^/]*}}
** {lappend pats {.*}}
*** {
#zero or more whole segments - adjacent *** duplicates collapse to one
if {[lindex $pats end] ne $zom} {
lappend pats $zom
}
}
default {
set seg [string map [list ^ {\^} $ {\$} \[ {\[} \] {\]} ( {\(} ) {\)} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters (or tcl glob square bracket chars) in the input as literals
#set seg [string map [list . {[.]}] $seg]
@ -759,7 +767,34 @@ namespace eval punk::path {
}
}
}
return "^[join $pats /]\$"
#join with / - except around zero-or-more markers, which must absorb one adjacent
#separator (a plain join cannot express 'zero segments'):
# X/*** -> ^X(?:/.*)?$ (trailing marker absorbs the preceding separator)
# ***/X a/***/b -> (?:.*/)?X ... (marker carries its trailing separator inside the
# optional group; no separator follows the marker)
# *** -> ^.*$
set n [llength $pats]
set out ""
for {set i 0} {$i < $n} {incr i} {
set pat [lindex $pats $i]
set is_zom [expr {$pat eq $zom}]
set is_last [expr {$i == $n - 1}]
if {$i > 0 && [lindex $pats $i-1] ne $zom && !($is_zom && $is_last)} {
append out /
}
if {$is_zom} {
if {$n == 1} {
append out {.*}
} elseif {$is_last} {
append out {(?:/.*)?}
} else {
append out {(?:.*/)?}
}
} else {
append out $pat
}
}
return "^${out}\$"
}
punk::args::define {
@ -775,10 +810,12 @@ namespace eval punk::path {
with 1 or more segments in between (so it will not match /usr/bin).
A pattern such as /usr/** will match any path that has /usr as the first segment, with 1 or more segments
following (so it will not match /usr itself).
A pattern such as /usr/*** will match /usr itself as well as any path below /usr
- *** as a whole segment matches zero or more segments (added 2026-07-20, G-093).
A pattern such as **/*.txt will match any path that ends with .txt, with 1 or more leading segments
(so it will not match test.txt or .txt).
(so it will not match test.txt or .txt). Use ***/*.txt to also match a bare test.txt.
A pattern such as ** will match any path.
The glob characters * and ? are the only special characters in the pathglob syntax.
The glob characters * and ? (and the whole-segment forms ** and ***) are the only special characters in the pathglob syntax.
- they are treated as glob characters regardless of where they appear in the pathglob string.
Note that this is different from other Tcl glob contexts where square brackets can be used.
The pathglob syntax treats other characters, including square brackets as literals.
@ -849,12 +886,12 @@ namespace eval punk::path {
set ismatch [regexp $re $path] ;#explicit -nocase 0 - require exact match of path literals including driveletter
} else {
#caller is using default for -nocase - which indicates case sensitivity - but we have an exception for the driveletter.
set re_segments [file split $re] ;#Note that file split c:/etc gives {c:/ etc} but file split ^c:/etc gives {^c: etc}
set first_seg [lindex $re_segments 0]
if {[regexp {^\^(.{1}):$} $first_seg _match driveletter]} {
#first part of re is like "^c:" i.e a drive letter
set chars [string tolower $driveletter][string toupper $driveletter]
set re [join [concat "^\[$chars\]:" [lrange $re_segments 1 end]] /] ;#rebuild re with case insensitive driveletter only - use join - not file join. file join will misinterpret leading re segment.
#Direct head rewrite of the anchored regex: if the pattern begins with a windows
#drive segment (^c:...) make just the drive letter class-insensitive.
#(The previous file-split of the regex text broke on forms where a group is glued
# to the drive - e.g ^c:(?:/.*)?$ from c:/*** - and allowed non-alpha 'drives'.)
if {[regexp {^\^([A-Za-z]):(.*)$} $re _match driveletter rest]} {
set re "^\[[string tolower $driveletter][string toupper $driveletter]\]:$rest"
}
#puts stderr "-->re: $re"
set ismatch [regexp $re $path]
@ -1142,7 +1179,10 @@ namespace eval punk::path {
set pattern_head [lindex $pattern_parts 0]
set path_head [lindex $path_parts 0]
if {$pattern_head eq "**"} {
if {$pattern_head eq "**" || $pattern_head eq "***"} {
#both spanning forms admit consuming zero path segments here (viability only
#asks whether a match below remains possible - *** additionally full-matches
#with zero segments, which globmatchpath handles)
if {[_pattern_prefix_viable_parts [lrange $pattern_parts 1 end] $path_parts]} {
return 1
}
@ -1160,6 +1200,10 @@ namespace eval punk::path {
}
proc pattern_boundary {pattern} {
#the directory at which a trailing-** pattern's subtree begins (that directory
#itself does not full-match X/** - the boundary branch grants descent+allbelow
#without including its files). Trailing-*** patterns need no boundary: they
#[titledesc {Filesystem path utilities}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk path filesystem utils}] [comment {-- Description at end of page heading --}]
@ -725,9 +725,10 @@ namespace eval punk::path {
proc pathglob_as_re {pathglob} {
#*** !doctools
#[call [fun pathglob_as_re] [arg pathglob]]
#[para] Returns a regular expression for matching a path to a glob pattern which can contain glob chars *|? in any segment of the path structure
#[para] Returns a regular expression for matching a path to a glob pattern which can contain glob chars *|**|***|? in any segment of the path structure
#[para] Does not support square bracket globs or character classes.
#[para] ** matches any number of subdirectories.
#[para] ** as a whole segment matches one or more segments.
#[para] *** as a whole segment matches zero or more segments - e.g /etc/*** will match /etc itself as well as anything below it. (added 2026-07-20, G-093)
#[para] e.g /etc/**/*.txt will match any .txt files at any depth below /etc (except directly within /etc itself)
#[para] e.g /etc/**.txt will match any .txt files at any depth below /etc
#[para] any segment that does not contain ** must match exactly one segment in the path
@ -738,6 +739,7 @@ namespace eval punk::path {
#todo - consider whether a way to escape the glob chars ? * is practical - to allow literals ? *
# - would require counting immediately-preceding backslashes
set zom "\x00zom\x00" ;#zero-or-more-segments marker for whole-segment *** (segment regexes never contain \x00)
set pats [list]
foreach seg [file split $pathglob] {
if {[string range $seg end end] eq "/"} {
@ -746,6 +748,12 @@ namespace eval punk::path {
switch -- $seg {
* {lappend pats {[^/]*}}
** {lappend pats {.*}}
*** {
#zero or more whole segments - adjacent *** duplicates collapse to one
if {[lindex $pats end] ne $zom} {
lappend pats $zom
}
}
default {
set seg [string map [list ^ {\^} $ {\$} \[ {\[} \] {\]} ( {\(} ) {\)} \{ \\\{ \\ {\\}] $seg] ;#treat regex characters (or tcl glob square bracket chars) in the input as literals
#set seg [string map [list . {[.]}] $seg]
@ -759,7 +767,34 @@ namespace eval punk::path {
}
}
}
return "^[join $pats /]\$"
#join with / - except around zero-or-more markers, which must absorb one adjacent
#separator (a plain join cannot express 'zero segments'):
# X/*** -> ^X(?:/.*)?$ (trailing marker absorbs the preceding separator)
# ***/X a/***/b -> (?:.*/)?X ... (marker carries its trailing separator inside the
# optional group; no separator follows the marker)
# *** -> ^.*$
set n [llength $pats]
set out ""
for {set i 0} {$i < $n} {incr i} {
set pat [lindex $pats $i]
set is_zom [expr {$pat eq $zom}]
set is_last [expr {$i == $n - 1}]
if {$i > 0 && [lindex $pats $i-1] ne $zom && !($is_zom && $is_last)} {
append out /
}
if {$is_zom} {
if {$n == 1} {
append out {.*}
} elseif {$is_last} {
append out {(?:/.*)?}
} else {
append out {(?:.*/)?}
}
} else {
append out $pat
}
}
return "^${out}\$"
}
punk::args::define {
@ -775,10 +810,12 @@ namespace eval punk::path {
with 1 or more segments in between (so it will not match /usr/bin).
A pattern such as /usr/** will match any path that has /usr as the first segment, with 1 or more segments
following (so it will not match /usr itself).
A pattern such as /usr/*** will match /usr itself as well as any path below /usr
- *** as a whole segment matches zero or more segments (added 2026-07-20, G-093).
A pattern such as **/*.txt will match any path that ends with .txt, with 1 or more leading segments
(so it will not match test.txt or .txt).
(so it will not match test.txt or .txt). Use ***/*.txt to also match a bare test.txt.
A pattern such as ** will match any path.
The glob characters * and ? are the only special characters in the pathglob syntax.
The glob characters * and ? (and the whole-segment forms ** and ***) are the only special characters in the pathglob syntax.
- they are treated as glob characters regardless of where they appear in the pathglob string.
Note that this is different from other Tcl glob contexts where square brackets can be used.
The pathglob syntax treats other characters, including square brackets as literals.
@ -849,12 +886,12 @@ namespace eval punk::path {
set ismatch [regexp $re $path] ;#explicit -nocase 0 - require exact match of path literals including driveletter
} else {
#caller is using default for -nocase - which indicates case sensitivity - but we have an exception for the driveletter.
set re_segments [file split $re] ;#Note that file split c:/etc gives {c:/ etc} but file split ^c:/etc gives {^c: etc}
set first_seg [lindex $re_segments 0]
if {[regexp {^\^(.{1}):$} $first_seg _match driveletter]} {
#first part of re is like "^c:" i.e a drive letter
set chars [string tolower $driveletter][string toupper $driveletter]
set re [join [concat "^\[$chars\]:" [lrange $re_segments 1 end]] /] ;#rebuild re with case insensitive driveletter only - use join - not file join. file join will misinterpret leading re segment.
#Direct head rewrite of the anchored regex: if the pattern begins with a windows
#drive segment (^c:...) make just the drive letter class-insensitive.
#(The previous file-split of the regex text broke on forms where a group is glued
# to the drive - e.g ^c:(?:/.*)?$ from c:/*** - and allowed non-alpha 'drives'.)
if {[regexp {^\^([A-Za-z]):(.*)$} $re _match driveletter rest]} {
set re "^\[[string tolower $driveletter][string toupper $driveletter]\]:$rest"
}
#puts stderr "-->re: $re"
set ismatch [regexp $re $path]
@ -1142,7 +1179,10 @@ namespace eval punk::path {
set pattern_head [lindex $pattern_parts 0]
set path_head [lindex $path_parts 0]
if {$pattern_head eq "**"} {
if {$pattern_head eq "**" || $pattern_head eq "***"} {
#both spanning forms admit consuming zero path segments here (viability only
#asks whether a match below remains possible - *** additionally full-matches
#with zero segments, which globmatchpath handles)
if {[_pattern_prefix_viable_parts [lrange $pattern_parts 1 end] $path_parts]} {
return 1
}
@ -1160,6 +1200,10 @@ namespace eval punk::path {
}
proc pattern_boundary {pattern} {
#the directory at which a trailing-** pattern's subtree begins (that directory
#itself does not full-match X/** - the boundary branch grants descent+allbelow
#without including its files). Trailing-*** patterns need no boundary: they