Browse Source

script subcommand: lib:<name> scriptlib support; punk::path 0.2.2 - 0.4.0

app-punkscript accepts lib:<name> (with or without .tcl - extensionless
appends it) as the shell subcommand does: `punkexe script lib:hello` or
bare `punkexe lib:hello` via reclassification. The prefix always wins; a
literal path beginning lib: (pathological - colon is illegal in Windows
filenames) is reachable via ./lib:... Not-found errors list every
searched location; only .tcl runs via the script subcommand.

Resolution policy is factored into punk::path::scriptlib_resolve
(PUNKARGS-documented) rather than copied inline: kit-internal
app/scriptlib first and deliberately not externally overridable, then
scriptlib dirs relative to the executable - the same policy
app-punkshell encodes inline (todo noted: refactor the shell path onto
the shared proc). Share definitions via modules, not launcher control
flow.

Verified on both generations (punk902z, punksys): lib:hello
extensionless and explicit, bare-arg reclassification, not-found
candidate listing (bin/scriptlib then <root>/scriptlib for a bin/ exe).
Project version 0.4.0 (backward-compatible behaviour addition).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
b8371e0587
  1. 5
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 38
      scriptlib/tktimer.tcl
  4. 2
      src/lib/AGENTS.md
  5. 31
      src/lib/app-punkscript/punkscript.tcl
  6. 88
      src/modules/punk/path-999999.0a1.0.tm
  7. 6
      src/modules/punk/path-buildversion.txt
  8. 31
      src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl
  9. 94
      src/vfs/_vfscommon.vfs/modules/punk/path-0.2.2.tm

5
CHANGELOG.md

@ -5,6 +5,11 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.4.0] - 2026-07-07
- `script` subcommand supports `lib:<name>` scriptlib scripts (with or without `.tcl` extension), matching the `shell` subcommand's scheme: `punkexe script lib:hello`, or bare `punkexe lib:hello` via reclassification. The prefix always wins — a literal path beginning `lib:` (pathological; illegal on Windows filesystems) is reachable via `./lib:...`. Not-found errors list the searched locations. Only `.tcl` runs via the script subcommand.
- punk::path 0.2.2: new `scriptlib_resolve` — the shared `lib:` resolution policy (kit-internal `app/scriptlib` first and not externally overridable, then scriptlib dirs relative to the executable), PUNKARGS-documented. app-punkshell still carries its inline copy of the policy (refactor onto the shared proc noted as todo).
## [0.3.1] - 2026-07-07
- `script` subcommand: core-command documentation parity with the interactive shell — app-punkscript now loads `punk::args::moduledoc::tclcore` (catch-guarded, ~40ms) so e.g. `'i list' | punkexe script` renders the doc table instead of "Undocumented command". Verified on both generations; kit startup ~0.33s.

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.3.1"
version = "0.4.0"

38
scriptlib/tktimer.tcl

@ -0,0 +1,38 @@
# timer.tcl - Tk countdown timer
# Usage: timer.tcl <seconds>
# Creates a Tk window displaying the remaining seconds, counts down to zero,
# then exits with code 0.
if {[catch {package require tk}]} {
package require Tk
}
if {$::argc < 1} {
puts stderr "Usage: [file tail [info script]] <seconds>"
exit 1
}
set seconds [lindex $::argv 0]
if {![string is integer -strict $seconds] || $seconds < 0} {
puts stderr "timer: seconds must be a non-negative integer (got '$seconds')"
exit 1
}
# Track the absolute end time so drift in `after` scheduling does not skew the count.
set ::timer_end [expr {[clock milliseconds] + $seconds * 1000}]
proc timer_tick {} {
set remaining [expr {$::timer_end - [clock milliseconds]}]
if {$remaining <= 0} {
.clock configure -text 0
exit 0
}
.clock configure -text [expr {int(($remaining + 999) / 1000)}]
after 100 timer_tick
}
label .clock -text $seconds -font {TkTextFont 48} -padx 40 -pady 30
pack .clock -expand 1 -anchor center
wm title . "Timer: $seconds s"
after 100 timer_tick

2
src/lib/AGENTS.md

@ -16,7 +16,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar
- Libraries here include:
- `app-punk/` — Punk REPL app entry (`app-punk::repl`)
- `app-punkshell/` — Punkshell app entry (`app-punkshell`); on eof of its input channel the `env(PUNK_PIPE_EOF)` policy (`exit`|`interactive`|unset=console heuristic) decides between terminating and reopening the console for an interactive repl — automated callers should set `exit` or end piped input with an explicit `exit`
- `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty
- `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:<name>` scriptlib script (resolved via `punk::path::scriptlib_resolve`), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty
- `app-shellspy/` — ShellSpy app entry (`app-shellspy`)
- `app_shell/` — Shell app helpers (`app_shell`)
- `app_shellrun/` — Shell run helpers (`app_shellrun`)

31
src/lib/app-punkscript/punkscript.tcl

@ -50,7 +50,36 @@ apply {{} {
#works with or without piped stdin present - the script may read stdin itself
set scriptname [lindex $arglist 0]
set scriptargs [lrange $arglist 1 end]
set scriptpath [file normalize $scriptname]
if {[string match -nocase lib:* $scriptname]} {
#scriptlib script: lib:<name> (one or more colons after lib), with or
#without .tcl extension. The prefix always wins - a literal relative
#path beginning 'lib:' (pathological; illegal on Windows filesystems
#anyway) can be reached with ./lib:... or an absolute path.
#Resolution policy is shared with the shell subcommand's scheme via
#punk::path::scriptlib_resolve (vfs-internal scriptlib wins and is
#not externally overridable, then scriptlib dirs relative to the exe).
set libname [string trimleft [string range $scriptname [string first : $scriptname]+1 end] :]
package require punk::path
if {[catch {punk::path::scriptlib_resolve $libname} resolved]} {
puts stderr "punk script: $resolved"
exit 1
}
set scriptpath [dict get $resolved path]
if {$scriptpath eq ""} {
puts stderr "punk script: scriptlib script '[dict get $resolved scriptname]' not found. Locations checked:"
foreach c [dict get $resolved candidates] {
puts stderr " $c"
}
exit 1
}
if {[string tolower [file extension $scriptpath]] ne ".tcl"} {
puts stderr "punk script: only .tcl scripts are supported via lib: in the script subcommand (resolved: $scriptpath)"
exit 1
}
set scriptpath [file normalize $scriptpath]
} else {
set scriptpath [file normalize $scriptname]
}
if {![file exists $scriptpath]} {
puts stderr "punk script: script file not found: '$scriptpath'"
exit 1

88
src/modules/punk/path-999999.0a1.0.tm

@ -485,6 +485,87 @@ namespace eval punk::path {
return relative
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::path::scriptlib_resolve
@cmd -name "punk::path::scriptlib_resolve"\
-summary\
"Resolve a scriptlib script name to a path using the punk kit search policy"\
-help\
"Resolves a relative script name (the part after a 'lib:' prefix
in punk executable launch arguments) against the scriptlib
locations, in the same order the punk shell uses:
1. the kit vfs internal scriptlib (<zipfs-root>/app/scriptlib,
or <exedir>/scriptlib for an unwrapped kit directory).
Internal scripts are deliberately NOT overridable by
external files (integrity and performance).
2. <exedir-parent>/scriptlib - e.g <projectroot>/scriptlib for
an executable in <projectroot>/bin.
3. <exedir-grandparent>/scriptlib.
If the name's extension (lowercased) is not one of the known
scriptlib extensions (.tcl .py .pl .ps1 .sh), '.tcl' is
appended - only .tcl scripts may be named extensionlessly.
Returns a dict with keys:
path - resolved absolute path, or empty string if not found
scriptname - the name searched for (extension applied)
candidates - the locations that were (or would be) checked,
for use in error messages.
Raises an error if name is not a relative path."
@leaders
name -type string -optional 0 -help\
"relative script name, with or without a known extension
(any leading lib: prefix should already be stripped)"
}]
}
proc scriptlib_resolve {name} {
#shared resolution policy for the 'lib:' script prefix - used by the punk
#executable 'script' subcommand (app-punkscript); app-punkshell's shell
#subcommand encodes the same policy (todo: refactor it onto this proc).
if {[lindex [pathtype $name] 0] ne "relative"} {
error "punk::path::scriptlib_resolve name must be a relative path (got '$name')"
}
set known_extensions [list .tcl .py .pl .ps1 .sh]
if {[string tolower [file extension $name]] ni $known_extensions} {
#only .tcl scripts allowed to be called extensionlessly
set scriptname $name.tcl
} else {
set scriptname $name
}
set exepath [file dirname [file normalize [file join [info nameofexecutable] ___]]] ;#symlink resolve
set candidates [list]
#1) kit-internal scriptlib - not overridable externally
set has_zipfs_command [expr {[info commands ::tcl::zipfs::root] ne ""}]
set kit_base ""
if {$has_zipfs_command && [file exists [tcl::zipfs::root]]} {
set kit_base [tcl::zipfs::root]
} elseif {[file type $exepath] eq "directory"} {
set kit_base $exepath
}
set kit_libdir ""
if {$has_zipfs_command && $kit_base ne "" && [file exists $kit_base/app/scriptlib]} {
set kit_libdir $kit_base/app/scriptlib
} elseif {[file exists $exepath/scriptlib]} {
set kit_libdir $exepath/scriptlib
}
if {$kit_libdir ne ""} {
lappend candidates $kit_libdir/$scriptname
if {[file exists $kit_libdir/$scriptname]} {
return [dict create path $kit_libdir/$scriptname scriptname $scriptname candidates $candidates]
}
}
#2,3) external scriptlib folders relative to the executable location
set exedir [file dirname $exepath]
foreach dir [list [file join $exedir scriptlib] [file join [file dirname $exedir] scriptlib]] {
lappend candidates [file join $dir $scriptname]
if {[file isdirectory $dir] && [file exists [file join $dir $scriptname]]} {
return [dict create path [file join $dir $scriptname] scriptname $scriptname candidates $candidates]
}
}
return [dict create path "" scriptname $scriptname candidates $candidates]
}
proc plain {str} {
set str [string map "\\\\ /" $str]
@ -1598,6 +1679,13 @@ namespace eval punk::path::system {
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
#documentation-only PUNKARGS registration - discovered lazily if/when punk::args loads
lappend ::punk::args::register::NAMESPACES ::punk::path::argdoc
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::path [namespace eval punk::path {

6
src/modules/punk/path-buildversion.txt

@ -1,6 +1,4 @@
0.2.1
0.2.2
#First line must be a semantic version number
#all other lines are ignored.
#0.2.1 - treefilenames: filter '.' and '..' by tail in the hidden-directory glob so recursion does not loop on non-Windows platforms
#0.2.0 - treefilenames now matches hidden files and recurses into hidden directories on all platforms
#0.2.0 - added PUNKARGS documentation for punk::path::relative
#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

31
src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl

@ -50,7 +50,36 @@ apply {{} {
#works with or without piped stdin present - the script may read stdin itself
set scriptname [lindex $arglist 0]
set scriptargs [lrange $arglist 1 end]
set scriptpath [file normalize $scriptname]
if {[string match -nocase lib:* $scriptname]} {
#scriptlib script: lib:<name> (one or more colons after lib), with or
#without .tcl extension. The prefix always wins - a literal relative
#path beginning 'lib:' (pathological; illegal on Windows filesystems
#anyway) can be reached with ./lib:... or an absolute path.
#Resolution policy is shared with the shell subcommand's scheme via
#punk::path::scriptlib_resolve (vfs-internal scriptlib wins and is
#not externally overridable, then scriptlib dirs relative to the exe).
set libname [string trimleft [string range $scriptname [string first : $scriptname]+1 end] :]
package require punk::path
if {[catch {punk::path::scriptlib_resolve $libname} resolved]} {
puts stderr "punk script: $resolved"
exit 1
}
set scriptpath [dict get $resolved path]
if {$scriptpath eq ""} {
puts stderr "punk script: scriptlib script '[dict get $resolved scriptname]' not found. Locations checked:"
foreach c [dict get $resolved candidates] {
puts stderr " $c"
}
exit 1
}
if {[string tolower [file extension $scriptpath]] ne ".tcl"} {
puts stderr "punk script: only .tcl scripts are supported via lib: in the script subcommand (resolved: $scriptpath)"
exit 1
}
set scriptpath [file normalize $scriptpath]
} else {
set scriptpath [file normalize $scriptname]
}
if {![file exists $scriptpath]} {
puts stderr "punk script: script file not found: '$scriptpath'"
exit 1

94
src/vfs/_vfscommon.vfs/modules/punk/path-0.2.1.tm → src/vfs/_vfscommon.vfs/modules/punk/path-0.2.2.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::path 0.2.1
# Application punk::path 0.2.2
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -17,7 +17,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::path 0 0.2.1]
#[manpage_begin punkshell_module_punk::path 0 0.2.2]
#[copyright "2023"]
#[titledesc {Filesystem path utilities}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk path filesystem utils}] [comment {-- Description at end of page heading --}]
@ -485,6 +485,87 @@ namespace eval punk::path {
return relative
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::path::scriptlib_resolve
@cmd -name "punk::path::scriptlib_resolve"\
-summary\
"Resolve a scriptlib script name to a path using the punk kit search policy"\
-help\
"Resolves a relative script name (the part after a 'lib:' prefix
in punk executable launch arguments) against the scriptlib
locations, in the same order the punk shell uses:
1. the kit vfs internal scriptlib (<zipfs-root>/app/scriptlib,
or <exedir>/scriptlib for an unwrapped kit directory).
Internal scripts are deliberately NOT overridable by
external files (integrity and performance).
2. <exedir-parent>/scriptlib - e.g <projectroot>/scriptlib for
an executable in <projectroot>/bin.
3. <exedir-grandparent>/scriptlib.
If the name's extension (lowercased) is not one of the known
scriptlib extensions (.tcl .py .pl .ps1 .sh), '.tcl' is
appended - only .tcl scripts may be named extensionlessly.
Returns a dict with keys:
path - resolved absolute path, or empty string if not found
scriptname - the name searched for (extension applied)
candidates - the locations that were (or would be) checked,
for use in error messages.
Raises an error if name is not a relative path."
@leaders
name -type string -optional 0 -help\
"relative script name, with or without a known extension
(any leading lib: prefix should already be stripped)"
}]
}
proc scriptlib_resolve {name} {
#shared resolution policy for the 'lib:' script prefix - used by the punk
#executable 'script' subcommand (app-punkscript); app-punkshell's shell
#subcommand encodes the same policy (todo: refactor it onto this proc).
if {[lindex [pathtype $name] 0] ne "relative"} {
error "punk::path::scriptlib_resolve name must be a relative path (got '$name')"
}
set known_extensions [list .tcl .py .pl .ps1 .sh]
if {[string tolower [file extension $name]] ni $known_extensions} {
#only .tcl scripts allowed to be called extensionlessly
set scriptname $name.tcl
} else {
set scriptname $name
}
set exepath [file dirname [file normalize [file join [info nameofexecutable] ___]]] ;#symlink resolve
set candidates [list]
#1) kit-internal scriptlib - not overridable externally
set has_zipfs_command [expr {[info commands ::tcl::zipfs::root] ne ""}]
set kit_base ""
if {$has_zipfs_command && [file exists [tcl::zipfs::root]]} {
set kit_base [tcl::zipfs::root]
} elseif {[file type $exepath] eq "directory"} {
set kit_base $exepath
}
set kit_libdir ""
if {$has_zipfs_command && $kit_base ne "" && [file exists $kit_base/app/scriptlib]} {
set kit_libdir $kit_base/app/scriptlib
} elseif {[file exists $exepath/scriptlib]} {
set kit_libdir $exepath/scriptlib
}
if {$kit_libdir ne ""} {
lappend candidates $kit_libdir/$scriptname
if {[file exists $kit_libdir/$scriptname]} {
return [dict create path $kit_libdir/$scriptname scriptname $scriptname candidates $candidates]
}
}
#2,3) external scriptlib folders relative to the executable location
set exedir [file dirname $exepath]
foreach dir [list [file join $exedir scriptlib] [file join [file dirname $exedir] scriptlib]] {
lappend candidates [file join $dir $scriptname]
if {[file isdirectory $dir] && [file exists [file join $dir $scriptname]]} {
return [dict create path [file join $dir $scriptname] scriptname $scriptname candidates $candidates]
}
}
return [dict create path "" scriptname $scriptname candidates $candidates]
}
proc plain {str} {
set str [string map "\\\\ /" $str]
@ -1598,12 +1679,19 @@ namespace eval punk::path::system {
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
#documentation-only PUNKARGS registration - discovered lazily if/when punk::args loads
lappend ::punk::args::register::NAMESPACES ::punk::path::argdoc
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::path [namespace eval punk::path {
variable pkg punk::path
variable version
set version 0.2.1
set version 0.2.2
}]
return
Loading…
Cancel
Save