punk::path 0.5.0:
- subfolders1 REMOVED - no call sites repo-wide; subfolders is the reviewed implementation
- subfolders ***-tail -exclude-paths patterns now classify as pruning subtree rules: X/*** is handled as the equivalent pair {X/** X} - identical results (the *** descend test alone omits and prunes the boundary), walk no longer descends excluded subtrees (~250x measured on a 1110-dir excluded subtree); bare *** exclude short-circuits like bare **
- strip_prefixdepth fixed: called the nonexistent 'norm' since its 2024-08-12 copy from punk::repo::path_strip_prefixdepth (kettle::path::strip lineage) - now uses the textual punk::path::normjoin; depth-only no-content-compare contract kept; prefix-exhausts-path boundary still errors (zero-arg file join), characterised not changed
- missing punk::winpath dependency declared (normjoin calls is_dos_device_path on every input - errored in a minimal interp that required only punk::path)
- PUNKARGS consistency pass: *|**|***|? path glob syntax centralised in argdoc::PATHGLOB_SEGMENT_SYNTAX (tstr placeholder fragment) shared across pathglob_as_re/globmatchpath/subfolders/treefilenames help; new argdocs for pathglob_as_re, normjoin and strip_prefixdepth; globmatchpath summary corrected (*** added) and doctools [fun] markup removed from punk::args help; treefilenames no longer claims unsorted results (-sort and zipfs dispatch documented), -exclude-paths/-exclude-files/-include-paths typed -type list, tail globs documented as ordinary Tcl globs
- path.test 40->67: coverage for previously-untested in-use functions (relative, normjoin, pathtype, scriptlib_resolve, treefilenames_zipfs with zipfs mkzip/mount fixture) plus globmatchpath ?/-nocase variants, treefilenames -exclude-files and -sort order pins, subfolders ***-tail/pair-equivalence/guard/classifier pins, strip_prefixdepth characterisation
Docs/conventions:
- src/modules/AGENTS.md: PUNKARGS Procedure Documentation Template converted to the -& record-continuation layout; -& stated as preferred for new work (existing blocks may keep backslash continuations)
- punk::path argdoc blocks converted to the aligned -& layout
- src/modules/punk/AGENTS.md: shared glob-syntax fragment mechanism (flush-left authoring rule, resolved_def consumer caveat) + subfolders1 removal recorded
punkshell 0.49.0 (punkproject.toml + CHANGELOG.md entry).
Verified: path.test 67/67 under tclsh90; all 10 documented punk::path ids render clean on punk91 (tcl9) and punk86 (8.6); punk::LOC resolved_def consumer of treefilenames -exclude-paths intact.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
@ -36,6 +36,7 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima
- punk::console powershell console-mode fallback (G-106, achieved 2026-07-22): on twapi-less windows runtimes, enableRaw/disableRaw are served by a persistent pwsh/powershell named-pipe server (`punk::console::system::ps_consolemode_*` procs). Contract points: the server starts lazily on first use (module load must never spawn processes or emit noise), is a process-wide singleton via tsv `punk_console ps_server_*` under `tsv::lock`, watches the owning process pid and exits with it (no orphan cleanup needed), and is quiet by default (`PUNK_PS_CONSOLEMODE_DEBUG=1` for diagnostics both sides; the spawn redirects child stdout/stderr to NUL but stdin MUST stay inherited - the console input handle is how the server reaches the console). Script resolution: env `PUNK_PS_CONSOLEMODE_SCRIPT` -> argv0-derived -> module-dir-derived -> the embedded copy in the module; the canonical maintained script is `scriptlib/utils/pwsh/consolemode_server_async.ps1` and the embedded copy must stay in sync with it - pinned by `src/tests/modules/punk/console/testsuites/console/psfallback.test`, so edit both together (delivery is powershell `-c <text>`, deliberately not a file: kits carry nothing on disk and ExecutionPolicy cannot block it). Verification recipe: goals/archive/G-106-powershell-consolemode-fallback.md Notes.
- punk::console has a console ownership registry (G-007): `console_owner_register`/`console_owner_get`/`console_owner_forget`, tsv `punk_console_owners` keyed by canonical {in out} pair. Ownership is captured when an opunk::console instance is anchored (via `::opunk::console::lifecycle_callback`, wired by `ensure_object_integration`) and by `default_console`; an unregistered console reads as "operate locally". For `{stdin stdout}` first registration wins and only the owner's forget releases the entry. Owner liveness is validated at consult time. The `dec_has_mode`/`ansi_has_mode` caches live in tsv `punk_console_modecache`.
- punk::console terminal queries are owner-routed (G-007 choke-point brokering): `internal::get_ansi_response_payload` consults `internal::console_route_owner` after spec resolution and forwards the whole call to the console-owning thread via synchronous `thread::send` when the caller is not the owner, so queueing, raw-mode cycling and cooperative reader handling execute in the owner's context and every query proc above the choke point inherits the routing. Routing applies to the default console `{stdin stdout}` only: non-std channel names are thread-local, so an {in out} pair spec names the calling thread's own console and always operates locally (unregistered/self-owned/dead-owner likewise - single-interp behaviour is unchanged). The synchronous send relies on the owner servicing events while the caller blocks (the repl does this while a codethread runs - same property as the repl-installed vt52/colour/mode aliases, which are unaffected because a call arriving in the owner resolves to owner==self). Tests live in `src/tests/modules/punk/console/testsuites/console/ownerrouting.test`.
- punk::path glob-pattern documentation is centralised (2026-08-03, path 0.5.0): the `*|**|***|?` path glob syntax text lives in the `::punk::path::argdoc::PATHGLOB_SEGMENT_SYNTAX` variable, referenced from PUNKARGS `-help` bodies via `${$::punk::path::argdoc::PATHGLOB_SEGMENT_SYNTAX}` placeholders (author such shared text with continuation lines flush-left - substitution prepends the placeholder line's indent). Extend it rather than re-duplicating glob syntax text in punk::path definitions. External definitions may pull punk::path option defs via `punk::args::resolved_def` (`punk::LOC` pulls treefilenames' `-exclude-paths`), so option help must not depend on surrounding-command context (e.g say "see punk::path::pathglob_as_re", not "as described above"). `subfolders1` was removed at path 0.5.0 (subfolders is the only folder-listing function; older snapshot tiers still carry it until promotion) and subfolders' ***-tail excludes decompose internally to pruning subtree rules - do not reintroduce a non-pruning *** path.
- Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs.
- Private helpers go in `namespace eval private { ... }` blocks.
#Continuation lines are deliberately unindented: placeholder substitution
#prepends the placeholder line's own indentation to every line after the
#first, so indented continuations here would render double-indented.
variable PATHGLOB_SEGMENT_SYNTAX {The glob elements *, **, *** and ? may appear in any segment of a path pattern:
* matches any single segment, or any span of characters within a segment
(/usr/*/bin matches /usr/local/bin but not /usr/bin or /usr/a/b/bin;
/usr/te*t matches /usr/test and /usr/teeeet but not /usr/te/t)
** as a whole segment matches 1 or more segments
(/usr/**/bin matches /usr/x/bin and /usr/x/y/bin but not /usr/bin.
Within a segment ** also spans segments: /usr/**.txt matches .txt files
at any depth below /usr)
*** as a whole segment matches 0 or more segments
(/usr/*** matches /usr itself as well as everything below it)
? matches any single character within a segment
(/usr/te?t matches /usr/test and /usr/text but not /usr/texxt or /usr/te/t)
* and ? are treated as glob characters wherever they appear in the pattern
- escaping is not supported. All other characters, including square brackets,
match as literals (unlike Tcl glob patterns).}
}
punk::args::define {
@id -id ::punk::path::pathglob_as_re
@cmd -name punk::path::pathglob_as_re -&
-summary -&
"Return a regular expression for matching paths to a *|**|***|? path glob pattern." -&
-help -&
"Returns an anchored regular expression string for matching a path to a
glob pattern which can contain the glob elements *|**|***|? in any
segment of the path structure.
${$::punk::path::argdoc::PATHGLOB_SEGMENT_SYNTAX}
The pathglob doesn't have to contain glob characters - a pathglob
without them produces a regex matching that path exactly.
Regular expression syntax is deliberately not supported within the
pathglob string - supplied regex characters are treated as literals.
This is the engine behind punk::path::globmatchpath and the path
pattern matching of the treefilenames/subfolders family."
@leaders -min 1 -max 1
pathglob -type string -optional 0 -help -&
"path glob pattern"
}
proc pathglob_as_re {pathglob} {
#see PUNKARGS id ::punk::path::pathglob_as_re (documentation-only - single positional arg)
#*** !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
@ -799,52 +918,40 @@ namespace eval punk::path {
punk::args::define {
@id -id ::punk::path::globmatchpath
@cmd -name punk::path::globmatchpath\
-summary\
"Match path to *|**|? glob patterns"\
-help\
"Return a boolean indicating whether the path matches the specialised glob pattern.
A pattern such as /usr/*/bin will match any path that has /usr as the first segment and bin as the third segment,
with any single segment in between.
A pattern such as /usr/**/bin will match any path that has /usr as the first segment and bin as the last segment,
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). Use ***/*.txt to also match a bare test.txt.
A pattern such as ** will match any path.
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.
For example, the pattern /usr/te?t will match /usr/test and /usr/text but not /usr/texxt, and the pattern /usr/te*t
will match /usr/test, /usr/teat, and /usr/teeeet but not /usr/te/t.
The pathglob syntax does not support escaping of glob characters - any glob characters in the pathglob are treated
as glob characters. For example, the pattern /usr/* will match any path that has /usr as the first segment and any
single segment as the second segment, but there is no way to specify a pattern that matches any path that has /usr
as the first segment and a literal * as the second segment.
Caller must ensure that file separator is forward slash. (e.g use file normalize on windows)
options:
-nocase 0|1 (default 0 - case sensitive)
If -nocase is not supplied - default to case sensitive *except for driveletter*
ie - the driveletter alone in paths such as c:/etc will still be case insensitive. (ie c:/ETC/* will match C:/ETC/blah but not C:/etc/blah)
Explicitly specifying -nocase 0 will require the entire case to match including the driveletter.
"
@cmd -name punk::path::globmatchpath -&
-summary -&
"Match path to *|**|***|? glob patterns" -&
-help -&
"Return a boolean indicating whether the path matches the specialised
glob pattern.
${$::punk::path::argdoc::PATHGLOB_SEGMENT_SYNTAX}
Further whole-pattern examples:
/usr/** will match any path that has /usr as the first segment and 1
or more segments following (so it will not match /usr itself).
/usr/*** will match /usr itself as well as any path below /usr.
**/*.txt will match any path that ends with .txt, with 1 or more
leading segments (so it will not match test.txt or .txt). Use
***/*.txt to also match a bare test.txt.
** will match any path.
Caller must ensure that file separator is forward slash. (e.g use
file normalize on windows)"
@leaders
pathglob -type string -help "glob pattern to match path against. See [fun pathglob_as_re] for syntax of glob patterns"
pathglob -type string -help -&
"glob pattern to match path against (syntax as described above -
punk::path::pathglob_as_re documents the underlying conversion)"
path -type string -help "path to match against glob pattern"
@opts
-nocase -type boolean -default 0 -help\
-nocase -type boolean -default 0 -help -&
"case insensitive matching (default false - case sensitive)
- except for driveletter on windows which is always case insensitive
unless -nocase 0 is explicitly specified"
- except for the driveletter on windows paths: when -nocase is not
supplied, matching is case sensitive EXCEPT for a leading
driveletter (c:/ETC/* will match C:/ETC/blah but not C:/etc/blah).
Explicitly specifying -nocase 0 requires the entire case to match
A single /*/ will match any single segment in the path, and a single /**/ will match any number of segments in the path.
e.g to exclude any path with _aside as a segment in the middle: -exclude-paths **/_aside/**
i.e this would exclude /usr/_aside/etc and /usr/x/_aside/etc but not /usr/x/_aside or _aside/etc
To exclude all paths with _aside as a segment anywhere: -exclude-paths { **/_aside/** **/_aside _aside/**}
"
#todo -depth
@values -min 0 -max 1
path -type directory -optional 1 -help\
"Path of folder. If not supplied current directory is used.
This may be a relative or absolute path. Relative paths are treated as relative to current directory.
When using relative paths - the result will also be relative paths with the same relative prefix.
(e.g if path is ../test - the results will be ../test/subfolder1 ../test/subfolder2 etc)
Patterns in -exclude-paths are matched against the resulting paths
(so should be written to match the same relative prefix if path is relative)"
}
proc subfolders1 {args} {
#NOTE - this algorithm based on omit_only_patterns and prune_base_patterns was suggested by a 2026 AI model - it is apparent to this programmer that it is inadequate for the purpose.
#This can still return something like c:/repo/etc/src/vfs - which should be excluded by the pattern **/src/**
#todo - review and fix properly.
set argd [punk::args::parse $args withid ::punk::path::subfolders1]
lassign [dict values $argd] leaders opts values received
set do_recursion [dict exists $received -recursive]
set exclude_paths [dict get $opts -exclude-paths]
if {"**" in $exclude_paths} {
#if ** is in exclude_paths - then we can skip all glob matching and just return empty list
#This is likely user error - so we'll be loud about it for now but will still return empty list rather than erroring.
#If user code is building exclude_paths dynamically - they can check for this case themselves and avoid the call to subfolders1 to suppress this message.
puts stderr "punk::path::subfolders1 Warning - exclude_paths contains '**' - all paths will be excluded"
return [list]
}
if {[dict exists $received path]} {
set path [dict get $values path]
} else {
set path [pwd]
}
set all_subfolders [glob -nocomplain -directory $path -types d *]
#example of expected exclude_paths pattern behaviour when recursion is enabled:
# **/dirname -> omit /x/y/dirname, but still visit /x/y/dirname/*
# **/dirname/* -> include /x/y/dirname and /x/y/dirname/a/b but omit directories that are a single level below /x/y/dirname such as /x/y/dirname/a
#c:/** - would exclude all subfolders below c: but not c: itself
# **/test/** - would exclude any path with test as a segment and all its subfolders
#- but not paths with test as a segment that is the final segment
set folders [list]
set recurse_subdirs [list]
foreach f $all_subfolders {
set include_in_results 1
set allow_recurse 1
foreach pat $exclude_paths {
set pat_parts [file split $pat] ;#note file split c:/test gives {c:/ test} but file split **/test gives {** test}
#also note that file split on windows treats forward slashes and backslashes the same.
#by using file split, we gain some flexibility in syntax of paths and patterns,
#but lose the ability to use backslashes as escapes to allow literal glob characters in path segments.
#This is almost always a non-issue on windows since * and ? are not valid in path segments there, and is rarely an issue on unix even though
# * and ? are technically valid in path segments, but it is inadvisable there anyway for compatibility with shells etc.
#0.5.0 - subfolders1 REMOVED (no call sites repo-wide; subfolders is the reviewed implementation - any external caller of the removed name must switch to subfolders); subfolders ***-tail -exclude-paths patterns now classify as pruning subtree rules (X/*** handled as the equivalent pair {X/** X}: identical results, walk no longer descends excluded subtrees - measured ~250x on a 1110-dir excluded subtree); bare *** exclude now short-circuits like bare ** (warning + empty list); punk::winpath dependency now declared (normjoin calls punk::winpath::is_dos_device_path on every input but the module never required it - normjoin errored in a minimal interp); strip_prefixdepth FIXED - its normalizer call was the nonexistent 'norm', broken since the 2024-08-12 copy from punk::repo::path_strip_prefixdepth (whose filesystem-normalizing norm exists there; kettle::path::strip lineage) - now uses the textual punk::path::normjoin (relative inputs stay relative; same depth-only no-content-compare contract; prefix depth >= path depth still errors via zero-arg file join, as punk::repo's own comments note); normjoin and strip_prefixdepth gain PUNKARGS argdocs; path.test extended 40->67 tests covering the in-use functions previously untested (relative, normjoin, pathtype, scriptlib_resolve, treefilenames_zipfs with zipfs-mount fixture) plus treefilenames -exclude-files/-sort modes, globmatchpath ?/-nocase variants, subfolders ***-tail/pair-equivalence/guard/classifier pins, and strip_prefixdepth/normjoin-edge characterisation
#0.5.0 - PUNKARGS consistency pass (same changeset): path glob syntax (*|**|***|?) centralised in argdoc::PATHGLOB_SEGMENT_SYNTAX tstr fragment shared across pathglob_as_re/globmatchpath/subfolders/treefilenames help; *** and ? documented for subfolders -exclude-paths (always supported via globmatchpath) incl per-form exclusion semantics; globmatchpath summary corrected (*** added) and doctools [fun] markup removed from punk::args help text; treefilenames help no longer claims unsorted results (-sort documented, zipfs dispatch noted), -exclude-files/tailglobs documented as ordinary Tcl globs, -type list added to -exclude-paths/-exclude-files/-include-paths; new documentation-only argdoc for pathglob_as_re
#0.4.0 - pathglob syntax: *** as a whole segment matches ZERO or more segments (X/*** = X and everything below; ***/f.txt matches f.txt at any depth incl root) - globmatchpath/pathglob_as_re/treefilenames (G-093); treefilenames include separability fix: an exact or single-segment-glob full match no longer sets allbelow (previously X or X/* dragged the whole subtree in via the deep walk; zipfs walk already separable); treefilenames exclude separability fix: only **|***-tail exclude patterns prune the subtree - other matches exclude just that folder's files and the walk continues below (both walks; previously any dir match pruned, contradicting the **/_aside vs **/_aside/** argdoc distinction); globmatchpath default-nocase driveletter rewrite now a direct regex-head rewrite (handles c:/*** glued-group form, restricts to alpha drives); treefilenames legacy bare-* include collapse removed (a * in -include-paths no longer means match-everything - it is the one-below lattice form; use **|***)
#0.3.0 - scriptlib_resolve: extensionless tcl scripts resolvable via first-line identification (new system::scriptfile_is_tcl helper - '# tcl' comment, tclsh/tclkit/wish shebang, or sh-trampoline); per-directory spelling order (exact name as called wins over .tcl-appended within each location, kit-internal dir precedence preserved); .kit added to known extensions (previously lib:x.kit wrongly searched x.kit.tcl); return dict keys reworked: scripttype (tcl/kit/py/pl/ps1/sh or empty) + notes added, scriptname now the matched relative name; directories no longer match a spelling (file isfile); now also used by app-punkshell/app_shellrun lib: handling (previously app-punkscript only)
#0.2.2 - added scriptlib_resolve: shared resolution policy for the lib:<script> prefix (kit-internal app/scriptlib first and not externally overridable, then scriptlib dirs relative to the executable); PUNKARGS argdoc namespace + registration added to the module