Browse Source

G-169/G-170 follow-on landed: coherent-set promotion + bake - launcher help depth reaches the kits (punkshell 0.56.0)

Both goals' open coherent-set promotion follow-ons name the SAME
vfscommonupdate/bake event; this change-set is that event, so both flip to
landed 2026-08-06 in the same work unit (the sanctioned archived-file edit).

Promotion (canonical release-sequence steps 5-8, native tclsh90 driver):

- mint: 'make.tcl packages -confirm 0'.
- promote: 'make.tcl bootsupport -confirm 0' - punk::args 0.17.0 -> 0.18.0 and
  punk::ansi 0.1.3 -> 0.2.0. The second pass copied zero modules (the gate's
  own contract) and 'make.tcl check' no longer reports stale bootsupport.
- promote: 'make.tcl vfscommonupdate -confirm 0' - the same two modules plus
  punkboot core 0.7.0 (byte-identical to src/vfs/_config/punkboot_core.tcl),
  punkexe moduledoc 0.6.1 -> 0.9.0, punkboot moduledoc 0.3.0 -> 0.3.1, and
  app-punkscript/app-punkscript-docs 1.1 -> 1.3 (pkgIndex + punkscript.tcl;
  vfscommon never carried the intermediate 1.2). Every promoted copy verified
  byte-identical to its mint output.
- bake: 'make.tcl bake -confirm 0' - INSTALLED KITS (11), no FAILED KITS. The
  templates modpod payload (both bootsupport and vfscommon copies) regenerated
  from the G-170 layout core edits.

What a deployed kit now does that it could not before, probed directly against
bin/punk902z.exe (no src package mode, no fake-main harness):

- 'help script oneliner' narrows to the oneliner form's argument table - the
  '-e' row, which is absent from form 0's default table - exit 0.
- the multi-form synopsis lines carry the un-suppressed formhint labels
  (' <punkexe> help script file' / ' <punkexe> help script oneliner'), G-170's
  resolution of the 0.6.1 circularity objection, rendered from the app-owned
  definition G-169 moved beside the code that implements it.
- 'help script somefile.tcl' dry-runs: the matched form plus the
  punk::args::parse_report landing report (Argument|Source|Value carrying
  'scriptfile received somefile.tcl') - G-151's render consumed by the core.
- 'help buildinfo bogus' rejects with exit 1, byte-empty stdout and the parse
  diagnosis on stderr; 'help shell somearg' keeps the passthrough
  accepted-and-ignored render at exit 0.
- 'help tclsh somefile somearg' now DRY-RUNS (accepted, exit 0) where the baked
  0.6.1 moduledoc had kept the historic ignored-tail render: the G-170 fence
  witness requires a -regexprefail row in the definition actually served, and
  the promoted 0.9.0 moduledoc supplies it. That was the recorded
  mixed-vintage state, now resolved in the deployed kits.

Verification:

- launcherhelp.test 38/38 against the freshly baked kits. The two direct-kit
  trailing-word pins that G-170 re-described as VINTAGE-PROOF hold under the
  depth core exactly as designed.
- Full source-tree suite (-jobs 16): 1560 passed / 20 skipped / 1 failed. The
  single failure is core/tcl/testsuites/tests/exec.test exec-14.3, a vendored
  Tcl core test asserting stock's 'exec -gorp' error text without -encoding -
  a Tcl 9.0.3 core-vintage mismatch, unrelated to this change-set.
- punkexe subtree 162 passed / 4 skipped / 0 failed. The 10 failures recorded
  as pre-existing through the G-168/G-169/G-170 sessions (maketclbakelist 2 +
  maketcllibfetch 8) now ALL PASS - they were stale build-state artifacts, and
  this mint/promote/bake refresh is precisely what they were waiting for. Worth
  weighing against the G-139 libfetch-test reading still pending a user call.
- punk/args subtree 337 passed / 3 skipped / 0 failed, its recorded baseline.
- Bake warnings are the known set only: punkmagic skipped (runtime
  tclsh90magic.exe absent from the store) and the zint.dll i386-in-x64 arch
  mismatch in the tcl8 kits punkbi/punksys/punk86, the recorded pre-existing
  payload defect. No configured kit exe was locked (the running punk901t.exe
  instances are not a configured bake target).

Project version 0.55.1 -> 0.56.0 with a CHANGELOG entry: the G-151/G-168/G-169/
G-170 launcher-family behaviour becomes user-visible in a deployed kit for the
first time with this bake, and none of those source commits bumped the project
version - the source change alone shipped nothing to a user.

ARCHITECTURE.md's launch-surface paragraph still described trailing help words
as merely 'accepted', which is pre-G-170 semantics, so that clause now states
the narrowing / dry-run / passthrough split; architecture_lint clean (66 path
refs, 62 goal refs). goals_lint clean (80 active / 90 archived).

Deliberately untouched and still open: G-151's make.tcl interim-report
retirement (its bootsupport gate is satisfied by this promotion, but the
make.tcl edit is separate work), G-169's app-ownership extension to
shell/punk/shellspy, and G-170's punkboot moduledoc authoring-hint note.

Claude-Session: https://claude.ai/code/session_01Y1diJnhjUxKgEG6EwYAzxj
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 4 days ago
parent
commit
5e956f7419
  1. 2
      ARCHITECTURE.md
  2. 21
      CHANGELOG.md
  3. 2
      goals/archive/G-169-app-owned-launch-docs.md
  4. 2
      goals/archive/G-170-launcher-help-depth.md
  5. 2
      punkproject.toml
  6. 107
      src/bootsupport/modules/punk/ansi-0.2.0.tm
  7. 460
      src/bootsupport/modules/punk/args-0.18.0.tm
  8. BIN
      src/bootsupport/modules/punk/mix/templates-0.2.0.tm
  9. 10
      src/vfs/_vfscommon.vfs/lib/app-punkscript/pkgIndex.tcl
  10. 125
      src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl
  11. 107
      src/vfs/_vfscommon.vfs/modules/punk/ansi-0.2.0.tm
  12. 460
      src/vfs/_vfscommon.vfs/modules/punk/args-0.18.0.tm
  13. 13
      src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.3.1.tm
  14. 187
      src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.9.0.tm
  15. BIN
      src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm
  16. 218
      src/vfs/_vfscommon.vfs/punkboot/core.tcl

2
ARCHITECTURE.md

@ -42,7 +42,7 @@ app package from src/lib/ (app-punkshell, app-punkscript, app-repl, app-shellsp
## Boot and launch chain
- **Kit anatomy.** Each `src/vfs/*.vfs` folder is a runtime payload; `src/runtime/mapvfs.toml` (G-024, tomlish-parsed; deprecated `mapvfs.config` line format still readable) maps payload folders to platform runtimes under `bin/runtime/<platform>/` as named kit outputs, with named groups and generative version-named schemes; an optional sibling `src/vfs/<name>.vfs.toml` declares payload packages materialized INTO the folder with drop-in-wins precedence (G-115 - spec: `src/vfs/README.md`); `src/vfs/_vfscommon.vfs/` is a generated merge of common libraries (never hand-edited); `src/vfs/_config/` holds the entry scripts. Sources: `src/vfs/AGENTS.md`, `src/runtime/AGENTS.md`.
- **Entry point.** `src/vfs/_config/punk_main.tcl` (thin main sourcing the shared boot core - G-031). An optional first argument is a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (`internal` is always appended when absent; `minted` was named `dev` pre-0.43.0), optionally scoped with the `proj:` prefix (G-033: `minted`/`src` resolve against the project containing the cwd - walk-up to the nearest git/fossil repo root with a punkshell-style src tree - instead of the executable's own project; discovery is always reported, never a silent rebind). The next argument is the subcommand: `shell`, `script`, `punk`, `tclsh`, `shellspy`, `buildinfo`, `help`. A non-subcommand first argument naming an existing file (or `lib:*` reference) is treated as a script invocation; a first argument of exactly `-e` runs the following argument as a Tcl one-liner (G-077, achieved - handled as `script -e`; tool-style kits keep `-e` routed to their own processor); anything else is refused with usage (G-032); no arguments at all means interactive shell. Subcommands map to app packages under `src/lib/` (`app-punkshell`, `app-punkscript`, `app-repl`, `app-shellspy`). The launch surface self-documents through punk::args (G-032; single-mechanism revision 2026-08-05 in boot core 0.5.0): `help ?subcommand? ?arg ...?` - the only help entry, trailing words accepted so `help` can be prepended to a command line - renders tabular usage from the moduledoc definitions plus project registrations (`::punkboot::project_subcommand_info` - summaries, definition ids, optional declared parse), degrading to a plain subcommand list when that stack is unavailable; help FLAGS are never intercepted (a leading `-help` earns the refusal, `<subcommand> -help` belongs to the subcommand). Source: `src/vfs/AGENTS.md`.
- **Entry point.** `src/vfs/_config/punk_main.tcl` (thin main sourcing the shared boot core - G-031). An optional first argument is a dash-delimited package mode composed of tokens from `minted`, `os`, `src`, `internal` (`internal` is always appended when absent; `minted` was named `dev` pre-0.43.0), optionally scoped with the `proj:` prefix (G-033: `minted`/`src` resolve against the project containing the cwd - walk-up to the nearest git/fossil repo root with a punkshell-style src tree - instead of the executable's own project; discovery is always reported, never a silent rebind). The next argument is the subcommand: `shell`, `script`, `punk`, `tclsh`, `shellspy`, `buildinfo`, `help`. A non-subcommand first argument naming an existing file (or `lib:*` reference) is treated as a script invocation; a first argument of exactly `-e` runs the following argument as a Tcl one-liner (G-077, achieved - handled as `script -e`; tool-style kits keep `-e` routed to their own processor); anything else is refused with usage (G-032); no arguments at all means interactive shell. Subcommands map to app packages under `src/lib/` (`app-punkshell`, `app-punkscript`, `app-repl`, `app-shellspy`). The launch surface self-documents through punk::args (G-032; single-mechanism revision 2026-08-05 in boot core 0.5.0): `help ?subcommand? ?arg ...?` - the only help entry, trailing words accepted so `help` can be prepended to a command line, with depth since G-170 (boot core 0.7.0): a single word naming one of a multi-form definition's forms (or a decimal ordinal) narrows the render to that form's argument table, any other tail on a parse-true subcommand is dry-run through its definition (accepted lines add the `punk::args::parse_report` landing report, rejected lines emit the parse diagnosis on stderr with exit 1), and passthrough subcommands keep the ignored tail - renders tabular usage from the moduledoc definitions plus project registrations (`::punkboot::project_subcommand_info` - summaries, definition ids, optional declared parse), degrading to a plain subcommand list when that stack is unavailable; help FLAGS are never intercepted (a leading `-help` earns the refusal, `<subcommand> -help` belongs to the subcommand). Source: `src/vfs/AGENTS.md`.
- **Package modes pick module provenance.** `internal` uses module snapshots baked into the kit at bake time; `minted` loads the mint output trees at repo-root `modules/` and `lib/`; `src` loads the unbuilt working tree (`src/modules`, `src/lib`, `src/bootsupport`, `src/vendormodules`) with `package prefer latest` so magic-version dev modules (`999999.0a1.0`) beat stamped snapshots. `src` mode is the standard way to verify working-tree changes without a rebuild. Source: `bin/AGENTS.md` "Launch package modes".
- **Payload mount derivation (G-129, achieved).** The boot keys zipfs presence on `tcl::zipfs::mount` and derives where the executable's attached archive actually mounted from the mount table itself (the entry whose archive file is the executable; fallback: the mount containing the boot script) instead of assuming `tcl::zipfs::root` + `//zipfs:/app`. Modern runtimes still answer `//zipfs:/app`; the androwish/undroidwish 8.6 backport has no `root` command, mounts at the executable's own path, and boots the same payload through its `app/main.tcl` in-archive hook (`src/vfs/punk8win.vfs` carries fauxlinks for both conventions). A mounted archive the boot cannot attribute is reported on stderr rather than silently yielding no internal paths. The derivation procs stay defined post-boot (`::punkboot::zipfs_kit_mountbase`). Sources: `src/vfs/AGENTS.md`, `bin/AGENTS.md` "Kit-wrappable runtime requirements", `src/tests/shell/testsuites/punkexe/kitmountpoint.test`.
- **Static package capture (G-058, achieved).** Boot probe-loads the runtime's statically linked packages in a throwaway interp and records what each provides; every interp the shell fabricates (boot, codethread, shellthread workers) seeds `package ifneeded <name> <ver> {load {} <prefix>}` from that record, and `punk::packagepreference` resolves static-vs-bundled version-aware. Sources: `src/vfs/AGENTS.md`, `goals/archive/G-058-static-runtime-packages.md`.

21
CHANGELOG.md

@ -5,6 +5,27 @@ 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.56.0] - 2026-08-06
- Coherent-set promotion + bake (the G-169 and G-170 follow-on promotion lines,
landed together): the launcher-family work of G-151, G-168, G-169 and G-170
reaches the kits. Promoted punk::args 0.18.0 and punk::ansi 0.2.0 into
bootsupport and `_vfscommon.vfs`, plus punkboot core 0.7.0, punkexe moduledoc
0.9.0, punkboot moduledoc 0.3.1 and app-punkscript/app-punkscript-docs 1.3
into `_vfscommon.vfs`; 11 kits rebaked and deployed. User-visible in a
deployed kit for the first time: `<punkexe> help <sub> <formname|index>`
narrows a multi-form definition to that form's argument table, the multi-form
synopsis lines carry actionable `help <sub> %formname%` labels instead of the
suppressed column, any other tail on a parse-true subcommand is dry-run
through its definition (accepted lines render the matched form plus the
punk::args::parse_report landing report; rejected lines emit the parse
diagnosis on stderr, exit 1), and `help script` renders the app-owned
definition that now lives beside the code implementing it.
- The tclsh dry-run arm goes live with this bake: it requires the G-168 fence
witness in the moduledoc actually served, so kits carrying the older 0.6.1
snapshot had kept the historic ignored-tail render. That mixed-vintage state
is now resolved in the deployed kits.
## [0.55.1] - 2026-08-05
- G-149 follow-on landed: make.tcl's multi-form definitions (tool, buildsuite)

2
goals/archive/G-169-app-owned-launch-docs.md

@ -149,5 +149,5 @@ Verification evidence:
## Follow-ons
Follow-on: the next vfscommonupdate/bake promotion carries the coherent set together - src/lib/app-punkscript (pkgIndex + punkscript.tcl 1.2), punkboot core 0.6.0 and punkexe moduledoc 0.8.0 (plus punkboot moduledoc 0.3.1) - after which the baked kit serves 'help script' from the app-owned definition in internal mode; mixed intermediate states degrade gracefully as recorded in Progress => open
Follow-on: the next vfscommonupdate/bake promotion carries the coherent set together - src/lib/app-punkscript (pkgIndex + punkscript.tcl 1.2), punkboot core 0.6.0 and punkexe moduledoc 0.8.0 (plus punkboot moduledoc 0.3.1) - after which the baked kit serves 'help script' from the app-owned definition in internal mode; mixed intermediate states degrade gracefully as recorded in Progress => landed 2026-08-06 (the same promotion + bake event that landed G-170's coherent-set line, by then carrying app-punkscript 1.3 / core 0.7.0 / punkexe moduledoc 0.9.0; the deployed kit's 'help script' now renders the app-owned definition in internal mode)
Follow-on: extend app ownership to the other app-handled built-ins (shell/punk via app-punkshell, shellspy via app-shellspy) whose moduledoc entries are summary-level passthrough contracts today - same docs-package pattern, launcher_builtin_docpackages gains their entries => open

2
goals/archive/G-170-launcher-help-depth.md

@ -167,5 +167,5 @@ Verification evidence:
## Follow-ons
Follow-on: coherent-set promotion - the same vfscommonupdate/bake event as G-169's open promotion line, now additionally carrying punkboot core 0.7.0 + punkexe moduledoc 0.9.0 + app-punkscript 1.3 (with punk::args 0.18.0 + punk::ansi 0.2.0 for the landing report); mixed intermediate states degrade gracefully by construction (fence witness, guarded parse_report) => open
Follow-on: coherent-set promotion - the same vfscommonupdate/bake event as G-169's open promotion line, now additionally carrying punkboot core 0.7.0 + punkexe moduledoc 0.9.0 + app-punkscript 1.3 (with punk::args 0.18.0 + punk::ansi 0.2.0 for the landing report); mixed intermediate states degrade gracefully by construction (fence witness, guarded parse_report) => landed 2026-08-06 (promotion + bake: bootsupport punk::args 0.18.0 + punk::ansi 0.2.0; _vfscommon.vfs punkboot core 0.7.0 + punkexe moduledoc 0.9.0 + punkboot moduledoc 0.3.1 + app-punkscript 1.3; 11 kits rebaked and deployed - the deployed kit now serves narrowing, formhint labels and the parse_report dry-run, and the tclsh fence witness is satisfied so its dry-run is live instead of the historic ignored-tail render)
Follow-on: punkboot moduledoc registration guidance - note at the 'argsid' key that a multi-form project definition may carry its own '@cmd -formhint " <punkexe> help <sub> %formname%"' template now that the launcher narrows on form words (project parse=1 dry-run and narrowing already work; only the authoring hint is missing) => open

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.55.1"
version = "0.56.0"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

107
src/bootsupport/modules/punk/ansi-0.1.3.tm → src/bootsupport/modules/punk/ansi-0.2.0.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::ansi 0.1.3
# Application punk::ansi 0.2.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -16,7 +16,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::ansi 0 0.1.3]
#[manpage_begin punkshell_module_punk::ansi 0 0.2.0]
#[copyright "2023"]
#[titledesc {Ansi string functions}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk Ansi library}] [comment {-- Description at end of page heading --}]
@ -10504,7 +10504,7 @@ tcl::namespace::eval punk::ansi::ansistring {
tcl::namespace::path [list ::punk::ansi ::punk::ansi::ta]
tcl::namespace::ensemble create
tcl::namespace::export length trim trimleft trimright INDEX INDEXCODE INDEXCHAR RANGE INSERT COUNT COUNT2 COUNT3 VIEW VIEWCODES VIEWSTYLE INDEXABSOLUTE INDEXCOLUMNS COLUMNINDEX NEW
tcl::namespace::export length trim trimleft trimright INDEX INDEXCODE INDEXCHAR RANGE TRUNCATE INSERT COUNT COUNT2 COUNT3 VIEW VIEWCODES VIEWSTYLE INDEXABSOLUTE INDEXCOLUMNS COLUMNINDEX NEW
#todo - expose _splits_ methods so caller can work efficiently with the splits themselves
#we need to consider whether these can be agnostic towards splits from split_codes vs split_codes_single
@ -11581,12 +11581,22 @@ tcl::namespace::eval punk::ansi::ansistring {
@id -id ::punk::ansi::ansistring::RANGE
@cmd -name punk::ansi::ansistring::RANGE\
-summary\
"Substring between grapheme positions, with ANSI codes preserved."\
"Substring between grapheme positions, with SGR styling re-applied."\
-help\
"Return the substring of string between grapheme positions startindex
and endindex inclusive (positions as for ansistring INDEX), including
the ANSI codes in effect for those graphemes.
end-relative index expressions are supported."
and endindex inclusive (positions as for ansistring INDEX), with the
ANSI SGR codes in effect for those graphemes re-applied at each
plain-text run - so the range renders as the original did.
The result is style-equivalent but not byte-identical: code stacks
are re-emitted in merged (canonicalized) form, only SGR codes are
carried (other sequence types - cursor movement, OSC, charset
shifts - are not re-emitted), and codes after the last requested
grapheme are not included (the SGR state after the whole string is
retrievable with ansistring INDEXCODE end+1).
end-relative index expressions are supported. An index beyond the
string clamps to the nearest end; if both indices fall beyond the
same end, or startindex resolves past endindex, an empty string is
returned (as for tcl string range)."
@values -min 3 -max 3
string -type string
startindex -type indexexpression
@ -11596,7 +11606,7 @@ tcl::namespace::eval punk::ansi::ansistring {
proc RANGE {string startindex endindex} {
#*** !doctools
#[call [fun RANGE] [arg string] [arg startindex] [arg endindex]]
#[para]Returns the substring of string between the character positions startindex and endindex, inclusive, where the character positions are determined by INDEX. The returned substring will include any ANSI codes that are in effect for those characters.
#[para]Returns the substring of string between the character positions startindex and endindex, inclusive, where the character positions are determined by INDEX. The ANSI SGR codes in effect for those characters are re-applied at each plain-text run in merged (canonicalized) form - style-equivalent but not byte-identical to the original; non-SGR codes and codes after the last requested character are not re-emitted (see INDEXCODE end+1 for trailing SGR state).
#TODO - special case processing for end+n and equivalents to return trailing ANSI codes as well.
#(similar to INDEXCODE)
@ -11697,6 +11707,85 @@ tcl::namespace::eval punk::ansi::ansistring {
return $rangeresult
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::ansi::ansistring::TRUNCATE
@cmd -name punk::ansi::ansistring::TRUNCATE\
-summary\
"Truncate to a maximum grapheme count, ANSI-aware, with an ellipsis marker."\
-help\
"Return string unchanged (byte-identical) if its grapheme count
(as for ansistring COUNT) is at most maxgraphemes - otherwise
return the leading graphemes with the ellipsis marker appended,
such that the result's total grapheme count is maxgraphemes.
The kept prefix is extracted as for ansistring RANGE, so SGR
codes in effect over the kept graphemes are re-applied in
merged (canonicalized) form - and when that prefix carries any
ANSI codes an SGR reset is appended before the marker so the
marker and any following text render unstyled.
The marker participates in the budget: the prefix keeps
maxgraphemes minus the marker's grapheme count. If the marker
alone meets or exceeds the budget, the result is the marker
itself truncated to maxgraphemes. maxgraphemes 0 returns an
empty string.
Truncation counts graphemes, not display columns - wide (e.g
CJK) graphemes count 1 each, as for COUNT."
@opts
-ellipsis -type string -default "..." -help\
"Marker appended when truncation occurs.
May itself contain ANSI codes; its grapheme count is charged
against maxgraphemes."
@values -min 2 -max 2
string -type string
maxgraphemes -type integer -help\
"Maximum grapheme count of the returned string (>= 0)."
} ]
}
proc TRUNCATE {args} {
#*** !doctools
#[call [fun TRUNCATE] [opt "-ellipsis marker"] [arg string] [arg maxgraphemes]]
#[para]Returns string unchanged (byte-identical) if its grapheme count (as for COUNT) is at most maxgraphemes - otherwise returns the leading graphemes with the marker (default ...) appended, such that the result's total grapheme count equals maxgraphemes.
#[para]The kept prefix is extracted as for RANGE (merged SGR re-application); when that prefix carries ANSI codes an SGR reset precedes the marker so the marker and any following text render unstyled. The marker's grapheme count is charged against maxgraphemes; a marker that alone meets or exceeds the budget is itself truncated to maxgraphemes. Grapheme-count based, not display-column based.
set ellipsis "..."
set arglist $args
while {[llength $arglist] > 2} {
switch -- [lindex $arglist 0] {
-ellipsis {
set ellipsis [lindex $arglist 1]
set arglist [lrange $arglist 2 end]
}
default {
error "ansistring TRUNCATE unknown option '[lindex $arglist 0]'. Known options: -ellipsis"
}
}
}
if {[llength $arglist] != 2} {
error "ansistring TRUNCATE requires: ?-ellipsis marker? string maxgraphemes"
}
lassign $arglist string maxgraphemes
if {![tcl::string::is integer -strict $maxgraphemes] || $maxgraphemes < 0} {
error "ansistring TRUNCATE maxgraphemes must be an integer >= 0. Received '$maxgraphemes'"
}
if {$maxgraphemes == 0} {
return ""
}
if {[COUNT $string] <= $maxgraphemes} {
return $string
}
set keep [expr {$maxgraphemes - [COUNT $ellipsis]}]
if {$keep <= 0} {
#marker alone meets/exceeds the budget - return as much of the marker as fits
return [RANGE $ellipsis 0 [expr {$maxgraphemes - 1}]]
}
set prefix [RANGE $string 0 [expr {$keep - 1}]]
if {[tcl::string::first \x1b $prefix] >= 0} {
#the prefix carries ANSI codes (RANGE re-applies SGR stacks with no trailing
#reset) - close styling so the marker and any following text render unstyled
append prefix "\x1b\[0m"
}
return $prefix$ellipsis
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::ansi::ansistring::INSERT
@ -13641,7 +13730,7 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::ansi [tcl::namespace::eval punk::ansi {
variable version
set version 0.1.3
set version 0.2.0
}]
return

460
src/bootsupport/modules/punk/args-0.17.0.tm → src/bootsupport/modules/punk/args-0.18.0.tm

@ -8,7 +8,7 @@
# (C) 2024
#
# @@ Meta Begin
# Application punk::args 0.17.0
# Application punk::args 0.18.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::args 0 0.17.0]
#[manpage_begin punkshell_module_punk::args 0 0.18.0]
#[copyright "2024"]
#[titledesc {args parsing}] [comment {-- Name section and table of contents description --}]
#[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}]
@ -810,6 +810,13 @@ tcl::namespace::eval punk::args {
which are unreachable from any repl - so the hint can
name that consumer's own navigation instead. Callers of
usage/arg_error may override with their -formhint option.
-reportvaluewidth <int>
Display cap (in graphemes) for the Value column of
punk::args::parse_report's landing table when a
successful parse of this definition is rendered.
0 means no cap. Callers of parse_report may override
with its -valuewidth option; with neither present the
built-in default (48) applies.
%B%@leaders%N% ?opt val...?
(used for leading args that come before switches/opts)
directive-options:
@ -8103,6 +8110,453 @@ tcl::namespace::eval punk::args {
return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r -formstatus $formstatus]
}
lappend PUNKARGS [list {
@id -id ::punk::args::parse_report
@cmd -name punk::args::parse_report\
-summary\
"Landing report for a successful parse - each value-holding argument with its source and value."\
-help\
"Render a compact landing table for a SUCCESSFUL parse - one row
per argument holding a value after the parse (received, or filled
from its -default), in declaration order by section (leaders, then
options, then values), with columns Argument, Source and Value.
This is the success-side sibling of the arg_error usage table:
where arg_error diagnoses a rejected line, parse_report shows
where each word of an accepted line landed - options consumed,
values attributed to the argument that took them (including a
flag-like word consumed as a VALUE by position), -multiple
aggregations, and received distinguished from defaulted via the
Source column (received / received xN / default). Arguments
holding no value (absent optionals with no -default) get no row.
The first argument is either the result dict of a prior successful
punk::args::parse (its id key must resolve to a registered
definition), or a list of words to parse followed by 'withid' and
the definition id. In the words form the parse is performed first,
and a validation failure raises the same error punk::args::parse
would - only successful parses have a landing report (see
arg_error/parse_status for failure presentation).
Value cells are display-elided to fit -valuewidth: strings are
rendered one-line (control characters as visible substitutes, as
for ansistring VIEW) and char-capped with a trailing '...' plus
' (len N)'; -multiple aggregations and -type list values are
element-capped with '(+N more)'; -type dict values are pair-capped
with '(+N pairs)'. Elision is display-only and always marked - the
-return dict form is never elided."
@form -form parsed
@leaders -min 1 -max 1
parsedresult -type dict -help\
"Result dict of a prior successful punk::args::parse
(keys: leaders opts values received solos multis id form)"
@form -form withid
@leaders -min 1 -max 1
arglist -type list -help\
"Words to parse - supplied as a single list"
@form -form {parsed withid}
@opts -prefix 0
-scheme -default info -choices {nocolour info error}
-return -default table -choices {table tableobject string dict} -choicelabels {
table "text of framed landing table"
tableobject "table object cmd"
string "plain lines, no table layout"
dict "machine form - never elided"
}
-title -type string -default "Received" -help\
"Title for the landing table frame."
-valuewidth -type integer -help\
"Display cap (graphemes) for the Value column - 0 means no cap.
When not supplied: a definition-level '@cmd -reportvaluewidth'
applies, else the built-in default 48. Deliberately never derived
from console width - report output is deterministic (and
pipe-safe) for a given parse and configuration. Elision markers
stay intact, so cells can exceed very small caps by the marker
width."
-caller -type string -default "" -help\
"Caller attribution for the validation failure message when the
words form's parse fails (see punk::args::parse -caller)."
@form -form parsed
@values -min 0 -max 0
@form -form withid
@values -min 2 -max 2
withid -type literal(withid) -help\
"The literal value 'withid'"
id -type string -help\
"id of punk::args definition for a command"
}]
proc parse_report {args} {
#G-151 annotated success render. Hand-rolled argument processing in the style of
#parse/parse_status (self-parse against the documented definition only on the
#unhappy path - so a report render never recurses through a second live parse).
if {[llength $args] < 1} {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
set payload [lindex $args 0]
set rest [lrange $args 1 end]
set opts [dict create {*}{
-scheme info
-return table
-title Received
-caller {}
}]
#-valuewidth deliberately absent from the defaults: absence selects the
#cascade (caller option > @cmd -reportvaluewidth > built-in 48)
set tail [list]
for {set i 0} {$i < [llength $rest]} {incr i} {
set k [lindex $rest $i]
if {$k eq "withid"} {
set tail [lrange $rest $i end]
break
}
switch -- $k {
-scheme - -return - -title - -valuewidth - -caller {
if {$i + 1 >= [llength $rest]} {
#missing option value - the documented definition raises
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
incr i
dict set opts $k [lindex $rest $i]
}
default {
#unknown option or stray word - the documented definition raises
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
}
switch -- [dict get $opts -return] {
table - tableobject - string - dict {}
default {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
set returntype [dict get $opts -return]
switch -- [dict get $opts -scheme] {
nocolour - nocolor {set scheme nocolour}
info {set scheme info}
error {set scheme error}
default {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
if {[llength $tail]} {
#words form - payload is the words, tail is: withid <id>
if {[llength $tail] != 2} {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
set id [lindex $tail 1]
if {[real_id $id] eq ""} {
error "punk::args::parse_report - no such id: $id"
}
set parseopts [list]
if {[dict get $opts -caller] ne ""} {
lappend parseopts -caller [dict get $opts -caller]
}
#a rejected line raises the same validation error punk::args::parse would -
#only successful parses have a landing report
set parsedresult [punk::args::parse $payload {*}$parseopts withid $id]
} else {
set parsedresult $payload
if {[catch {tcl::dict::size $parsedresult}] || ![tcl::dict::exists $parsedresult received]} {
error "punk::args::parse_report - first argument is not a punk::args::parse result dict (and no 'withid <id>' tail was supplied)"
}
}
#resolve the definition and the displayed form from the parse result
set docid [Dict_getdef $parsedresult id ""]
set real [real_id $docid]
if {$real eq ""} {
error "punk::args::parse_report - cannot resolve a registered definition from the parse result's id '$docid' - use the words form: parse_report <arglist> withid <id>"
}
set spec_dict [get_spec $real]
#G-046: expand display-deferred field content before display use (as arg_error does)
if {[tcl::dict::exists $spec_dict DISPLAY_DEFERRED] && [tcl::dict::size [tcl::dict::get $spec_dict DISPLAY_DEFERRED]]} {
set spec_dict [private::expand_display_fields $spec_dict]
}
set fid [Dict_getdef $parsedresult form 0]
if {![tcl::dict::exists $spec_dict FORMS $fid]} {
error "punk::args::parse_report - parse result names form '$fid' which is not present in definition '$real'"
}
set ARG_INFO [tcl::dict::get $spec_dict FORMS $fid ARG_INFO]
#-valuewidth cascade: caller option > @cmd -reportvaluewidth > built-in 48
if {[tcl::dict::exists $opts -valuewidth]} {
set valuewidth [tcl::dict::get $opts -valuewidth]
} else {
set valuewidth [Dict_getdef $spec_dict cmd_info -reportvaluewidth 48]
}
if {![tcl::string::is integer -strict $valuewidth] || $valuewidth < 0} {
error "punk::args::parse_report -valuewidth must be an integer >= 0 (0 means no cap). Received '$valuewidth'"
}
#per-argument landing data - parse_status_build inserts argstatus entries in
#declaration-section order (leaders, opts, values), which is the row order
set PSTAT [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $parsedresult]
set report_rows [tcl::dict::create]
tcl::dict::for {argname AST} [tcl::dict::get $PSTAT argstatus] {
set class [tcl::dict::get $AST class]
set received [tcl::dict::get $AST received]
set hasvalue [tcl::dict::get $AST hasvalue]
set value [tcl::dict::get $AST value]
if {!$hasvalue} {
#bridge the storage-key folds the build's direct ARG_INFO-name lookup
#misses: -parsekey renames the storage key; aliased optionsets store
#under their last |-member (the finalopts fold in get_dict). G-084 owns
#the storage-key model - this is display-level bridging only.
switch -- $class {
leader {set sectionkey leaders}
option {set sectionkey opts}
default {set sectionkey values}
}
set storekey [Dict_getdef $ARG_INFO $argname -parsekey ""]
if {$storekey eq "" && [tcl::string::first | $argname] >= 0} {
set storekey [lindex [split $argname |] end]
}
if {$storekey ne "" && [tcl::dict::exists $parsedresult $sectionkey $storekey]} {
set hasvalue 1
set value [tcl::dict::get $parsedresult $sectionkey $storekey]
}
}
if {!$hasvalue && $received == 0} {
continue ;#absent optional with no default-in-effect - no row
}
set is_multiple 0
if {[tcl::dict::exists $ARG_INFO $argname -multiple] && [tcl::dict::get $ARG_INFO $argname -multiple]} {
set is_multiple 1
}
tcl::dict::set report_rows $argname [tcl::dict::create\
class $class\
source [expr {$received > 0 ? "received" : "default"}]\
received $received\
positions [tcl::dict::get $AST positions]\
multiple $is_multiple\
hasvalue $hasvalue\
value $value\
]
}
if {$returntype eq "dict"} {
#machine form - never elided
return [tcl::dict::create id $real form $fid rows $report_rows]
}
#display forms - colour bootstrap as for arg_error (proc-local CLR seeded from
#the shared base array, scheme overrides merged into the local copy only;
#forced reload when the colour on/off state has flipped since load)
variable arg_error_CLR
set forcereload 0
if {[info exists arg_error_CLR(testsinglecolour)]} {
set terminal_colour_is_on [expr {[string length [a+ yellow]]}]
set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}]
if {$terminal_colour_is_on ^ $error_colour_is_on} {
set forcereload 1
}
}
private::argerror_load_colours $forcereload
array set CLR [array get arg_error_CLR]
switch -- $scheme {
nocolour {
variable arg_error_CLR_nocolour
array set CLR [array get arg_error_CLR_nocolour]
}
info {
variable arg_error_CLR_info
array set CLR [array get arg_error_CLR_info]
}
error {
variable arg_error_CLR_error
array set CLR [array get arg_error_CLR_error]
}
}
set RST "\x1b\[0m"
set display_rows [list]
tcl::dict::for {argname R} $report_rows {
set received [tcl::dict::get $R received]
if {$received > 0} {
set src received
if {$received > 1} {append src " x$received"}
set src_display "$CLR(check)$src$RST"
} else {
set src default
set src_display "$CLR(parsekey_hint)$src$RST"
}
set value_display ""
if {[tcl::dict::get $R hasvalue]} {
if {[tcl::dict::get $R multiple]} {
set mode list
} else {
set typelist [Dict_getdef $ARG_INFO $argname -type string]
if {"dict" in $typelist} {
set mode dict
} elseif {"list" in $typelist} {
set mode list
} else {
set mode string
}
}
set value_display [private::report_display_value [tcl::dict::get $R value] $mode $valuewidth]
}
lappend display_rows [list $argname $src $src_display $value_display]
}
if {![catch {package require textblock}]} {
set has_textblock 1
} else {
set has_textblock 0
}
if {$has_textblock && $returntype in {table tableobject}} {
set t [textblock::class::table new -title "$CLR(title)[tcl::dict::get $opts -title]$RST"]
$t add_column -headers [list Argument]
$t add_column -headers [list Source]
$t add_column -headers [list Value]
foreach row $display_rows {
lassign $row argname src src_display value_display
$t add_row [list $argname $src_display $value_display]
}
$t configure -show_hseps 0 {*}{
-show_header 1
} -ansibase_body $CLR(ansibase_body) {*}{
} -ansibase_header $CLR(ansibase_header) {*}{
} -ansiborder_header $CLR(ansiborder) {*}{
} -ansiborder_body $CLR(ansiborder)
if {$returntype eq "tableobject"} {
return $t
}
set out [$t print]
$t destroy
return $out
}
#plain lines - the string return, and the degraded path when textblock
#is unavailable
set lines [list]
foreach row $display_rows {
lassign $row argname src src_display value_display
lappend lines "$argname ($src) = $value_display"
}
return [join $lines \n]
}
#G-151 display-cell preparation for parse_report: single-line-ize, then type-aware
#elision with honest markers ('...' + ' (len N)' for strings, '(+N more)' for
#list/-multiple elements, '(+N pairs)' for dicts). budget is a grapheme cap for
#the cell (0 = no cap); markers stay intact, so a cell can exceed a very small
#budget by the marker width - deterministic for a given value and budget, never
#console-derived. ANSI/grapheme machinery via punk::ansi::ansistring when
#available - otherwise plain-string fallbacks (caps by chars, controls as
#backslash escapes).
proc private::report_display_value {value mode budget} {
set has_ansistring [expr {[llength [info commands ::punk::ansi::ansistring]] > 0}]
switch -- $mode {
list - dict {
if {[catch {llength $value} nelem]} {
#not a valid Tcl list - display as a plain string
set mode string
} elseif {$mode eq "dict" && $nelem % 2 != 0} {
#not a valid dict - display as a plain string
set mode string
}
}
}
switch -- $mode {
list {
set reprs [list]
foreach e $value {
lappend reprs [Report_singleline [list $e] $has_ansistring]
}
set full [join $reprs " "]
if {$budget == 0 || [Report_gcount $full $has_ansistring] <= $budget} {
return $full
}
lassign [Report_capped_elements $reprs $budget $has_ansistring] shown kept
return "[join $shown { }] (+[expr {[llength $reprs] - $kept}] more)"
}
dict {
set reprs [list]
foreach {k v} $value {
lappend reprs "[Report_singleline [list $k] $has_ansistring] [Report_singleline [list $v] $has_ansistring]"
}
set full [join $reprs " "]
if {$budget == 0 || [Report_gcount $full $has_ansistring] <= $budget} {
return $full
}
lassign [Report_capped_elements $reprs $budget $has_ansistring] shown kept
return "[join $shown { }] (+[expr {[llength $reprs] - $kept}] pairs)"
}
default {
set rawlen [tcl::string::length $value]
set disp [Report_singleline $value $has_ansistring]
if {$budget == 0 || [Report_gcount $disp $has_ansistring] <= $budget} {
return $disp
}
set lenmark " (len $rawlen)"
set cap [expr {$budget - [tcl::string::length $lenmark]}]
if {$cap < 1} {set cap 1}
return [Report_truncate $disp $cap $has_ansistring]$lenmark
}
}
}
#single-line-ize for report display: linefeed/tab/cr etc as visible substitutes.
#ansistring VIEW also renders any embedded ANSI escapes visibly - neutralizing
#them so a value cannot disturb the table layout. -sp 0: real spaces stay spaces.
proc private::Report_singleline {str has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring VIEW -lf 1 -vt 1 -sp 0 $str]
}
return [tcl::string::map [list \r\n {\r\n} \n {\n} \r {\r} \t {\t} \x1b {\e}] $str]
}
proc private::Report_gcount {str has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring COUNT $str]
}
return [tcl::string::length $str]
}
proc private::Report_truncate {str cap has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring TRUNCATE $str $cap]
}
if {[tcl::string::length $str] <= $cap} {
return $str
}
if {$cap <= 3} {
return [tcl::string::range "..." 0 [expr {$cap - 1}]]
}
return "[tcl::string::range $str 0 [expr {$cap - 4}]]..."
}
#whole-element capping for list/dict display: keep as many whole element reprs as
#fit a budget that reserves nominal room for the trailing count marker; always
#keep at least one (char-capped if it alone overflows). Returns {shown kept}.
proc private::Report_capped_elements {reprs budget has_ansistring} {
set reserve 12
set avail [expr {$budget - $reserve}]
if {$avail < 1} {set avail 1}
set shown [list]
set used 0
foreach repr $reprs {
if {[llength $shown] == 0} {
if {[Report_gcount $repr $has_ansistring] > $avail} {
set repr [Report_truncate $repr $avail $has_ansistring]
}
lappend shown $repr
set used [Report_gcount $repr $has_ansistring]
continue
}
set rlen [Report_gcount $repr $has_ansistring]
if {$used + 1 + $rlen > $avail} {
break
}
lappend shown $repr
incr used [expr {1 + $rlen}]
}
return [list $shown [llength $shown]]
}
#return number of values we can assign to cater for variable length clauses such as:
# {"elseif" expr "?then?" body}
#review - efficiency? each time we call this - we are looking ahead at the same info
@ -14627,7 +15081,7 @@ package provide punk::args [tcl::namespace::eval punk::args {
tcl::namespace::path {::punk::args::lib ::punk::args::system}
variable pkg punk::args
variable version
set version 0.17.0
set version 0.18.0
}]
return

BIN
src/bootsupport/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

10
src/vfs/_vfscommon.vfs/lib/app-punkscript/pkgIndex.tcl

@ -1 +1,9 @@
package ifneeded app-punkscript 1.1 [list source [file join $dir punkscript.tcl]]
package ifneeded app-punkscript 1.3 [list source [file join $dir punkscript.tcl]]
#G-169: docs-only load of the same file - the execution gate in punkscript.tcl
#returns before the app body, so requiring the docs never runs a script. Used
#by the launcher's help path (guarded lazy require in punkboot_core.tcl).
package ifneeded app-punkscript-docs 1.3 [list ::apply {{dir} {
namespace eval ::app::punkscript {variable docs_only 1}
source [file join $dir punkscript.tcl]
namespace eval ::app::punkscript {variable docs_only 0}
}} $dir]

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

@ -1,4 +1,127 @@
package provide app-punkscript 1.1
#G-169: launch-subcommand documentation lives WITH the parsing code it describes
#(co-location kills the doc-drift class): this file carries the
#(script)::punkexe::script punk::args definition and registers it inertly - a
#plain namespace-variable write plus a membership-guarded registration lappend.
#No package requires, no punk::args dependency, no definition evaluation here
#(the G-030 degradation doctrine; punk::args evaluates the definition lazily on
#the first help/usage query naming the id - the docpackages mechanism).
#Two load contexts share this file (see pkgIndex.tcl):
# package require app-punkscript the app: docs section, then the script
# runner body below
# package require app-punkscript-docs docs only - the launcher's help path
# (guarded lazy require in
# punkboot_core.tcl launcher_show_help):
# the execution gate below returns before
# the app body, so requiring the docs
# NEVER runs a script
#The docs section is idempotent (set + guarded lappend): a docs-then-app
#require sequence re-sources this file without duplicate registrations.
namespace eval ::app::punkscript::argdoc {
variable PUNKARGS
#single definer for (script)::punkexe::script (G-169 handover from the
#punkexe moduledoc, which no longer defines it). The G-168 selection-sound
#shape is contract - guarded by the args-tier punkexemodel.test /
#formcheck.test pins: the file form fences scriptfile against exactly '-e'
#(with the ./-e message), the oneliner form declares '-e' as a required
#solo flag so a bare '-e' verdicts viable-incomplete.
#NOTE: the braced definition below keeps the punkexe moduledoc's internal
#indentation (directives at 12, continuations at 16/18) - braced -help
#values carry their leading whitespace into the render. (The G-169
#handover moved the text byte-identical; the first deliberate post-
#handover divergence is G-170's -formhint template below.)
set PUNKARGS [list [list {
@id -id (script)::punkexe::script
#-formhint (G-170, replacing the interim suppression): the hint
#names real navigation - '<punkexe> help script <formname>' narrows
#the launcher's render to that form (exact form name, or decimal
#ordinal; a file literally named like a form stays dry-runnable as
#./oneliner - the ./-e reachability rule). Deliberately NO trailing
#ellipsis (display-hint honesty): words after the form word are not
#a continuation of the narrowed render - they flip the tail into
#the dry-run interpretation.
@cmd -name "punkexe script" -&
-summary -&
"Run a script non-interactively in the punk script environment." -&
-formhint " <punkexe> help script %formname%" -&
-help -&
"Run a Tcl script and exit with an honest exit code
(app-punkscript): script errors exit nonzero and the launch
plumbing emits nothing of its own on stdout/stderr, so
exec-style callers see only the script's output. The default
punk module/alias environment is loaded (unlike 'tclsh').
The script comes from the file argument, from piped stdin when
no argument is given (terminal stdin without a script argument
is a usage error - no interactive fallback), or via scriptlib
resolution when the argument has the form 'lib:name' (resolved
from the scriptlib locations associated with the executable,
including extensionless and shebang'd scripts).
A first argument of exactly '-e' runs the SECOND argument as a
Tcl one-liner (G-077): non-interactive, ::argv0 is '-e',
arguments after the script text land in ::argv, and a
non-empty final result is echoed to stdout (the same one-shot
eval ergonomics as the stdin form). '-e' with no following
script argument is a usage error - never an interactive
fall-through, and never stock tclsh's -e misparse (argv-swallow
plus stdin read). The form works with or without piped stdin
present; stdin is read only if the one-liner itself reads it.
A file literally named '-e' stays reachable as './-e'. The
same one-liner is available at the top level as
'<punkexe> -e <script> ?arg ...?'.
Help flags are ordinary script arguments in every position
(no interception - 2026-08-05 single-mechanism revision):
use '<punkexe> help script' for this usage."
@form -form {file} -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@values -min 0 -max -1
scriptfile -type string -optional 1 -&
-regexprefail {^-e$} -&
-regexprefailmsg -&
"the script subcommand's scriptfile may not be exactly '-e' (reserved for the -e one-liner form; a file literally named -e is reachable as ./-e)" -&
-help -&
"Script file path, or lib:name for scriptlib resolution.
Omitted: the script is read from piped stdin.
Any first word other than exactly '-e' is a file path here -
including other dash-led words ('-x' names a file, not an
option; G-168 selection-soundness: the fence on exactly '-e'
makes form auto-selection agree with dispatch)."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {oneliner} -synopsis "<punkexe> script -e <script> ?arg ...?"
@opts
-e -type none -optional 0 -help -&
"The one-liner selector (G-077): required, and only meaningful
as the first word after 'script' - a later '-e' is an
ordinary script/file argument (the file form applies). '-e'
with no following script argument is a usage error - never an
interactive fall-through. (G-168: declared as a required solo
flag so a bare '-e' verdicts the form viable-incomplete -
matching the app's 'requires a script argument' reality.)"
@values -min 1 -max -1
script -type string -optional 0 -help -&
"Tcl code to evaluate as a one-liner (G-077): errors print
errorInfo to stderr with exit 1; a non-empty final result
is echoed to stdout."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the one-liner in ::argv (::argv0 is '-e')."
}]]
}
namespace eval ::punk::args::register {
#inert docpackages registration (punk::args 0.12.7 lane): id-scanned on the
#next update_definitions, definitions evaluated only on a query naming them
variable NAMESPACES
if {![info exists NAMESPACES] || "::app::punkscript::argdoc" ni $NAMESPACES} {
lappend NAMESPACES ::app::punkscript::argdoc
}
}
package provide app-punkscript-docs 1.3
if {[info exists ::app::punkscript::docs_only] && $::app::punkscript::docs_only} {
#docs-only load context (app-punkscript-docs): stop before the app body
return
}
package provide app-punkscript 1.3
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015).
#
#Contract (G-015 - see goals/archive/G-015-script-subcommand-piped-stdin.md):

107
src/vfs/_vfscommon.vfs/modules/punk/ansi-0.1.3.tm → src/vfs/_vfscommon.vfs/modules/punk/ansi-0.2.0.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::ansi 0.1.3
# Application punk::ansi 0.2.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -16,7 +16,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::ansi 0 0.1.3]
#[manpage_begin punkshell_module_punk::ansi 0 0.2.0]
#[copyright "2023"]
#[titledesc {Ansi string functions}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk Ansi library}] [comment {-- Description at end of page heading --}]
@ -10504,7 +10504,7 @@ tcl::namespace::eval punk::ansi::ansistring {
tcl::namespace::path [list ::punk::ansi ::punk::ansi::ta]
tcl::namespace::ensemble create
tcl::namespace::export length trim trimleft trimright INDEX INDEXCODE INDEXCHAR RANGE INSERT COUNT COUNT2 COUNT3 VIEW VIEWCODES VIEWSTYLE INDEXABSOLUTE INDEXCOLUMNS COLUMNINDEX NEW
tcl::namespace::export length trim trimleft trimright INDEX INDEXCODE INDEXCHAR RANGE TRUNCATE INSERT COUNT COUNT2 COUNT3 VIEW VIEWCODES VIEWSTYLE INDEXABSOLUTE INDEXCOLUMNS COLUMNINDEX NEW
#todo - expose _splits_ methods so caller can work efficiently with the splits themselves
#we need to consider whether these can be agnostic towards splits from split_codes vs split_codes_single
@ -11581,12 +11581,22 @@ tcl::namespace::eval punk::ansi::ansistring {
@id -id ::punk::ansi::ansistring::RANGE
@cmd -name punk::ansi::ansistring::RANGE\
-summary\
"Substring between grapheme positions, with ANSI codes preserved."\
"Substring between grapheme positions, with SGR styling re-applied."\
-help\
"Return the substring of string between grapheme positions startindex
and endindex inclusive (positions as for ansistring INDEX), including
the ANSI codes in effect for those graphemes.
end-relative index expressions are supported."
and endindex inclusive (positions as for ansistring INDEX), with the
ANSI SGR codes in effect for those graphemes re-applied at each
plain-text run - so the range renders as the original did.
The result is style-equivalent but not byte-identical: code stacks
are re-emitted in merged (canonicalized) form, only SGR codes are
carried (other sequence types - cursor movement, OSC, charset
shifts - are not re-emitted), and codes after the last requested
grapheme are not included (the SGR state after the whole string is
retrievable with ansistring INDEXCODE end+1).
end-relative index expressions are supported. An index beyond the
string clamps to the nearest end; if both indices fall beyond the
same end, or startindex resolves past endindex, an empty string is
returned (as for tcl string range)."
@values -min 3 -max 3
string -type string
startindex -type indexexpression
@ -11596,7 +11606,7 @@ tcl::namespace::eval punk::ansi::ansistring {
proc RANGE {string startindex endindex} {
#*** !doctools
#[call [fun RANGE] [arg string] [arg startindex] [arg endindex]]
#[para]Returns the substring of string between the character positions startindex and endindex, inclusive, where the character positions are determined by INDEX. The returned substring will include any ANSI codes that are in effect for those characters.
#[para]Returns the substring of string between the character positions startindex and endindex, inclusive, where the character positions are determined by INDEX. The ANSI SGR codes in effect for those characters are re-applied at each plain-text run in merged (canonicalized) form - style-equivalent but not byte-identical to the original; non-SGR codes and codes after the last requested character are not re-emitted (see INDEXCODE end+1 for trailing SGR state).
#TODO - special case processing for end+n and equivalents to return trailing ANSI codes as well.
#(similar to INDEXCODE)
@ -11697,6 +11707,85 @@ tcl::namespace::eval punk::ansi::ansistring {
return $rangeresult
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::ansi::ansistring::TRUNCATE
@cmd -name punk::ansi::ansistring::TRUNCATE\
-summary\
"Truncate to a maximum grapheme count, ANSI-aware, with an ellipsis marker."\
-help\
"Return string unchanged (byte-identical) if its grapheme count
(as for ansistring COUNT) is at most maxgraphemes - otherwise
return the leading graphemes with the ellipsis marker appended,
such that the result's total grapheme count is maxgraphemes.
The kept prefix is extracted as for ansistring RANGE, so SGR
codes in effect over the kept graphemes are re-applied in
merged (canonicalized) form - and when that prefix carries any
ANSI codes an SGR reset is appended before the marker so the
marker and any following text render unstyled.
The marker participates in the budget: the prefix keeps
maxgraphemes minus the marker's grapheme count. If the marker
alone meets or exceeds the budget, the result is the marker
itself truncated to maxgraphemes. maxgraphemes 0 returns an
empty string.
Truncation counts graphemes, not display columns - wide (e.g
CJK) graphemes count 1 each, as for COUNT."
@opts
-ellipsis -type string -default "..." -help\
"Marker appended when truncation occurs.
May itself contain ANSI codes; its grapheme count is charged
against maxgraphemes."
@values -min 2 -max 2
string -type string
maxgraphemes -type integer -help\
"Maximum grapheme count of the returned string (>= 0)."
} ]
}
proc TRUNCATE {args} {
#*** !doctools
#[call [fun TRUNCATE] [opt "-ellipsis marker"] [arg string] [arg maxgraphemes]]
#[para]Returns string unchanged (byte-identical) if its grapheme count (as for COUNT) is at most maxgraphemes - otherwise returns the leading graphemes with the marker (default ...) appended, such that the result's total grapheme count equals maxgraphemes.
#[para]The kept prefix is extracted as for RANGE (merged SGR re-application); when that prefix carries ANSI codes an SGR reset precedes the marker so the marker and any following text render unstyled. The marker's grapheme count is charged against maxgraphemes; a marker that alone meets or exceeds the budget is itself truncated to maxgraphemes. Grapheme-count based, not display-column based.
set ellipsis "..."
set arglist $args
while {[llength $arglist] > 2} {
switch -- [lindex $arglist 0] {
-ellipsis {
set ellipsis [lindex $arglist 1]
set arglist [lrange $arglist 2 end]
}
default {
error "ansistring TRUNCATE unknown option '[lindex $arglist 0]'. Known options: -ellipsis"
}
}
}
if {[llength $arglist] != 2} {
error "ansistring TRUNCATE requires: ?-ellipsis marker? string maxgraphemes"
}
lassign $arglist string maxgraphemes
if {![tcl::string::is integer -strict $maxgraphemes] || $maxgraphemes < 0} {
error "ansistring TRUNCATE maxgraphemes must be an integer >= 0. Received '$maxgraphemes'"
}
if {$maxgraphemes == 0} {
return ""
}
if {[COUNT $string] <= $maxgraphemes} {
return $string
}
set keep [expr {$maxgraphemes - [COUNT $ellipsis]}]
if {$keep <= 0} {
#marker alone meets/exceeds the budget - return as much of the marker as fits
return [RANGE $ellipsis 0 [expr {$maxgraphemes - 1}]]
}
set prefix [RANGE $string 0 [expr {$keep - 1}]]
if {[tcl::string::first \x1b $prefix] >= 0} {
#the prefix carries ANSI codes (RANGE re-applies SGR stacks with no trailing
#reset) - close styling so the marker and any following text render unstyled
append prefix "\x1b\[0m"
}
return $prefix$ellipsis
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punk::ansi::ansistring::INSERT
@ -13641,7 +13730,7 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::ansi [tcl::namespace::eval punk::ansi {
variable version
set version 0.1.3
set version 0.2.0
}]
return

460
src/vfs/_vfscommon.vfs/modules/punk/args-0.17.0.tm → src/vfs/_vfscommon.vfs/modules/punk/args-0.18.0.tm

@ -8,7 +8,7 @@
# (C) 2024
#
# @@ Meta Begin
# Application punk::args 0.17.0
# Application punk::args 0.18.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::args 0 0.17.0]
#[manpage_begin punkshell_module_punk::args 0 0.18.0]
#[copyright "2024"]
#[titledesc {args parsing}] [comment {-- Name section and table of contents description --}]
#[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}]
@ -810,6 +810,13 @@ tcl::namespace::eval punk::args {
which are unreachable from any repl - so the hint can
name that consumer's own navigation instead. Callers of
usage/arg_error may override with their -formhint option.
-reportvaluewidth <int>
Display cap (in graphemes) for the Value column of
punk::args::parse_report's landing table when a
successful parse of this definition is rendered.
0 means no cap. Callers of parse_report may override
with its -valuewidth option; with neither present the
built-in default (48) applies.
%B%@leaders%N% ?opt val...?
(used for leading args that come before switches/opts)
directive-options:
@ -8103,6 +8110,453 @@ tcl::namespace::eval punk::args {
return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r -formstatus $formstatus]
}
lappend PUNKARGS [list {
@id -id ::punk::args::parse_report
@cmd -name punk::args::parse_report\
-summary\
"Landing report for a successful parse - each value-holding argument with its source and value."\
-help\
"Render a compact landing table for a SUCCESSFUL parse - one row
per argument holding a value after the parse (received, or filled
from its -default), in declaration order by section (leaders, then
options, then values), with columns Argument, Source and Value.
This is the success-side sibling of the arg_error usage table:
where arg_error diagnoses a rejected line, parse_report shows
where each word of an accepted line landed - options consumed,
values attributed to the argument that took them (including a
flag-like word consumed as a VALUE by position), -multiple
aggregations, and received distinguished from defaulted via the
Source column (received / received xN / default). Arguments
holding no value (absent optionals with no -default) get no row.
The first argument is either the result dict of a prior successful
punk::args::parse (its id key must resolve to a registered
definition), or a list of words to parse followed by 'withid' and
the definition id. In the words form the parse is performed first,
and a validation failure raises the same error punk::args::parse
would - only successful parses have a landing report (see
arg_error/parse_status for failure presentation).
Value cells are display-elided to fit -valuewidth: strings are
rendered one-line (control characters as visible substitutes, as
for ansistring VIEW) and char-capped with a trailing '...' plus
' (len N)'; -multiple aggregations and -type list values are
element-capped with '(+N more)'; -type dict values are pair-capped
with '(+N pairs)'. Elision is display-only and always marked - the
-return dict form is never elided."
@form -form parsed
@leaders -min 1 -max 1
parsedresult -type dict -help\
"Result dict of a prior successful punk::args::parse
(keys: leaders opts values received solos multis id form)"
@form -form withid
@leaders -min 1 -max 1
arglist -type list -help\
"Words to parse - supplied as a single list"
@form -form {parsed withid}
@opts -prefix 0
-scheme -default info -choices {nocolour info error}
-return -default table -choices {table tableobject string dict} -choicelabels {
table "text of framed landing table"
tableobject "table object cmd"
string "plain lines, no table layout"
dict "machine form - never elided"
}
-title -type string -default "Received" -help\
"Title for the landing table frame."
-valuewidth -type integer -help\
"Display cap (graphemes) for the Value column - 0 means no cap.
When not supplied: a definition-level '@cmd -reportvaluewidth'
applies, else the built-in default 48. Deliberately never derived
from console width - report output is deterministic (and
pipe-safe) for a given parse and configuration. Elision markers
stay intact, so cells can exceed very small caps by the marker
width."
-caller -type string -default "" -help\
"Caller attribution for the validation failure message when the
words form's parse fails (see punk::args::parse -caller)."
@form -form parsed
@values -min 0 -max 0
@form -form withid
@values -min 2 -max 2
withid -type literal(withid) -help\
"The literal value 'withid'"
id -type string -help\
"id of punk::args definition for a command"
}]
proc parse_report {args} {
#G-151 annotated success render. Hand-rolled argument processing in the style of
#parse/parse_status (self-parse against the documented definition only on the
#unhappy path - so a report render never recurses through a second live parse).
if {[llength $args] < 1} {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
set payload [lindex $args 0]
set rest [lrange $args 1 end]
set opts [dict create {*}{
-scheme info
-return table
-title Received
-caller {}
}]
#-valuewidth deliberately absent from the defaults: absence selects the
#cascade (caller option > @cmd -reportvaluewidth > built-in 48)
set tail [list]
for {set i 0} {$i < [llength $rest]} {incr i} {
set k [lindex $rest $i]
if {$k eq "withid"} {
set tail [lrange $rest $i end]
break
}
switch -- $k {
-scheme - -return - -title - -valuewidth - -caller {
if {$i + 1 >= [llength $rest]} {
#missing option value - the documented definition raises
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
incr i
dict set opts $k [lindex $rest $i]
}
default {
#unknown option or stray word - the documented definition raises
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
}
switch -- [dict get $opts -return] {
table - tableobject - string - dict {}
default {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
set returntype [dict get $opts -return]
switch -- [dict get $opts -scheme] {
nocolour - nocolor {set scheme nocolour}
info {set scheme info}
error {set scheme error}
default {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
}
if {[llength $tail]} {
#words form - payload is the words, tail is: withid <id>
if {[llength $tail] != 2} {
punk::args::parse $args withid ::punk::args::parse_report
error "punk::args::parse_report - internal argument processing mismatch for '$args'"
}
set id [lindex $tail 1]
if {[real_id $id] eq ""} {
error "punk::args::parse_report - no such id: $id"
}
set parseopts [list]
if {[dict get $opts -caller] ne ""} {
lappend parseopts -caller [dict get $opts -caller]
}
#a rejected line raises the same validation error punk::args::parse would -
#only successful parses have a landing report
set parsedresult [punk::args::parse $payload {*}$parseopts withid $id]
} else {
set parsedresult $payload
if {[catch {tcl::dict::size $parsedresult}] || ![tcl::dict::exists $parsedresult received]} {
error "punk::args::parse_report - first argument is not a punk::args::parse result dict (and no 'withid <id>' tail was supplied)"
}
}
#resolve the definition and the displayed form from the parse result
set docid [Dict_getdef $parsedresult id ""]
set real [real_id $docid]
if {$real eq ""} {
error "punk::args::parse_report - cannot resolve a registered definition from the parse result's id '$docid' - use the words form: parse_report <arglist> withid <id>"
}
set spec_dict [get_spec $real]
#G-046: expand display-deferred field content before display use (as arg_error does)
if {[tcl::dict::exists $spec_dict DISPLAY_DEFERRED] && [tcl::dict::size [tcl::dict::get $spec_dict DISPLAY_DEFERRED]]} {
set spec_dict [private::expand_display_fields $spec_dict]
}
set fid [Dict_getdef $parsedresult form 0]
if {![tcl::dict::exists $spec_dict FORMS $fid]} {
error "punk::args::parse_report - parse result names form '$fid' which is not present in definition '$real'"
}
set ARG_INFO [tcl::dict::get $spec_dict FORMS $fid ARG_INFO]
#-valuewidth cascade: caller option > @cmd -reportvaluewidth > built-in 48
if {[tcl::dict::exists $opts -valuewidth]} {
set valuewidth [tcl::dict::get $opts -valuewidth]
} else {
set valuewidth [Dict_getdef $spec_dict cmd_info -reportvaluewidth 48]
}
if {![tcl::string::is integer -strict $valuewidth] || $valuewidth < 0} {
error "punk::args::parse_report -valuewidth must be an integer >= 0 (0 means no cap). Received '$valuewidth'"
}
#per-argument landing data - parse_status_build inserts argstatus entries in
#declaration-section order (leaders, opts, values), which is the row order
set PSTAT [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $parsedresult]
set report_rows [tcl::dict::create]
tcl::dict::for {argname AST} [tcl::dict::get $PSTAT argstatus] {
set class [tcl::dict::get $AST class]
set received [tcl::dict::get $AST received]
set hasvalue [tcl::dict::get $AST hasvalue]
set value [tcl::dict::get $AST value]
if {!$hasvalue} {
#bridge the storage-key folds the build's direct ARG_INFO-name lookup
#misses: -parsekey renames the storage key; aliased optionsets store
#under their last |-member (the finalopts fold in get_dict). G-084 owns
#the storage-key model - this is display-level bridging only.
switch -- $class {
leader {set sectionkey leaders}
option {set sectionkey opts}
default {set sectionkey values}
}
set storekey [Dict_getdef $ARG_INFO $argname -parsekey ""]
if {$storekey eq "" && [tcl::string::first | $argname] >= 0} {
set storekey [lindex [split $argname |] end]
}
if {$storekey ne "" && [tcl::dict::exists $parsedresult $sectionkey $storekey]} {
set hasvalue 1
set value [tcl::dict::get $parsedresult $sectionkey $storekey]
}
}
if {!$hasvalue && $received == 0} {
continue ;#absent optional with no default-in-effect - no row
}
set is_multiple 0
if {[tcl::dict::exists $ARG_INFO $argname -multiple] && [tcl::dict::get $ARG_INFO $argname -multiple]} {
set is_multiple 1
}
tcl::dict::set report_rows $argname [tcl::dict::create\
class $class\
source [expr {$received > 0 ? "received" : "default"}]\
received $received\
positions [tcl::dict::get $AST positions]\
multiple $is_multiple\
hasvalue $hasvalue\
value $value\
]
}
if {$returntype eq "dict"} {
#machine form - never elided
return [tcl::dict::create id $real form $fid rows $report_rows]
}
#display forms - colour bootstrap as for arg_error (proc-local CLR seeded from
#the shared base array, scheme overrides merged into the local copy only;
#forced reload when the colour on/off state has flipped since load)
variable arg_error_CLR
set forcereload 0
if {[info exists arg_error_CLR(testsinglecolour)]} {
set terminal_colour_is_on [expr {[string length [a+ yellow]]}]
set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}]
if {$terminal_colour_is_on ^ $error_colour_is_on} {
set forcereload 1
}
}
private::argerror_load_colours $forcereload
array set CLR [array get arg_error_CLR]
switch -- $scheme {
nocolour {
variable arg_error_CLR_nocolour
array set CLR [array get arg_error_CLR_nocolour]
}
info {
variable arg_error_CLR_info
array set CLR [array get arg_error_CLR_info]
}
error {
variable arg_error_CLR_error
array set CLR [array get arg_error_CLR_error]
}
}
set RST "\x1b\[0m"
set display_rows [list]
tcl::dict::for {argname R} $report_rows {
set received [tcl::dict::get $R received]
if {$received > 0} {
set src received
if {$received > 1} {append src " x$received"}
set src_display "$CLR(check)$src$RST"
} else {
set src default
set src_display "$CLR(parsekey_hint)$src$RST"
}
set value_display ""
if {[tcl::dict::get $R hasvalue]} {
if {[tcl::dict::get $R multiple]} {
set mode list
} else {
set typelist [Dict_getdef $ARG_INFO $argname -type string]
if {"dict" in $typelist} {
set mode dict
} elseif {"list" in $typelist} {
set mode list
} else {
set mode string
}
}
set value_display [private::report_display_value [tcl::dict::get $R value] $mode $valuewidth]
}
lappend display_rows [list $argname $src $src_display $value_display]
}
if {![catch {package require textblock}]} {
set has_textblock 1
} else {
set has_textblock 0
}
if {$has_textblock && $returntype in {table tableobject}} {
set t [textblock::class::table new -title "$CLR(title)[tcl::dict::get $opts -title]$RST"]
$t add_column -headers [list Argument]
$t add_column -headers [list Source]
$t add_column -headers [list Value]
foreach row $display_rows {
lassign $row argname src src_display value_display
$t add_row [list $argname $src_display $value_display]
}
$t configure -show_hseps 0 {*}{
-show_header 1
} -ansibase_body $CLR(ansibase_body) {*}{
} -ansibase_header $CLR(ansibase_header) {*}{
} -ansiborder_header $CLR(ansiborder) {*}{
} -ansiborder_body $CLR(ansiborder)
if {$returntype eq "tableobject"} {
return $t
}
set out [$t print]
$t destroy
return $out
}
#plain lines - the string return, and the degraded path when textblock
#is unavailable
set lines [list]
foreach row $display_rows {
lassign $row argname src src_display value_display
lappend lines "$argname ($src) = $value_display"
}
return [join $lines \n]
}
#G-151 display-cell preparation for parse_report: single-line-ize, then type-aware
#elision with honest markers ('...' + ' (len N)' for strings, '(+N more)' for
#list/-multiple elements, '(+N pairs)' for dicts). budget is a grapheme cap for
#the cell (0 = no cap); markers stay intact, so a cell can exceed a very small
#budget by the marker width - deterministic for a given value and budget, never
#console-derived. ANSI/grapheme machinery via punk::ansi::ansistring when
#available - otherwise plain-string fallbacks (caps by chars, controls as
#backslash escapes).
proc private::report_display_value {value mode budget} {
set has_ansistring [expr {[llength [info commands ::punk::ansi::ansistring]] > 0}]
switch -- $mode {
list - dict {
if {[catch {llength $value} nelem]} {
#not a valid Tcl list - display as a plain string
set mode string
} elseif {$mode eq "dict" && $nelem % 2 != 0} {
#not a valid dict - display as a plain string
set mode string
}
}
}
switch -- $mode {
list {
set reprs [list]
foreach e $value {
lappend reprs [Report_singleline [list $e] $has_ansistring]
}
set full [join $reprs " "]
if {$budget == 0 || [Report_gcount $full $has_ansistring] <= $budget} {
return $full
}
lassign [Report_capped_elements $reprs $budget $has_ansistring] shown kept
return "[join $shown { }] (+[expr {[llength $reprs] - $kept}] more)"
}
dict {
set reprs [list]
foreach {k v} $value {
lappend reprs "[Report_singleline [list $k] $has_ansistring] [Report_singleline [list $v] $has_ansistring]"
}
set full [join $reprs " "]
if {$budget == 0 || [Report_gcount $full $has_ansistring] <= $budget} {
return $full
}
lassign [Report_capped_elements $reprs $budget $has_ansistring] shown kept
return "[join $shown { }] (+[expr {[llength $reprs] - $kept}] pairs)"
}
default {
set rawlen [tcl::string::length $value]
set disp [Report_singleline $value $has_ansistring]
if {$budget == 0 || [Report_gcount $disp $has_ansistring] <= $budget} {
return $disp
}
set lenmark " (len $rawlen)"
set cap [expr {$budget - [tcl::string::length $lenmark]}]
if {$cap < 1} {set cap 1}
return [Report_truncate $disp $cap $has_ansistring]$lenmark
}
}
}
#single-line-ize for report display: linefeed/tab/cr etc as visible substitutes.
#ansistring VIEW also renders any embedded ANSI escapes visibly - neutralizing
#them so a value cannot disturb the table layout. -sp 0: real spaces stay spaces.
proc private::Report_singleline {str has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring VIEW -lf 1 -vt 1 -sp 0 $str]
}
return [tcl::string::map [list \r\n {\r\n} \n {\n} \r {\r} \t {\t} \x1b {\e}] $str]
}
proc private::Report_gcount {str has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring COUNT $str]
}
return [tcl::string::length $str]
}
proc private::Report_truncate {str cap has_ansistring} {
if {$has_ansistring} {
return [punk::ansi::ansistring TRUNCATE $str $cap]
}
if {[tcl::string::length $str] <= $cap} {
return $str
}
if {$cap <= 3} {
return [tcl::string::range "..." 0 [expr {$cap - 1}]]
}
return "[tcl::string::range $str 0 [expr {$cap - 4}]]..."
}
#whole-element capping for list/dict display: keep as many whole element reprs as
#fit a budget that reserves nominal room for the trailing count marker; always
#keep at least one (char-capped if it alone overflows). Returns {shown kept}.
proc private::Report_capped_elements {reprs budget has_ansistring} {
set reserve 12
set avail [expr {$budget - $reserve}]
if {$avail < 1} {set avail 1}
set shown [list]
set used 0
foreach repr $reprs {
if {[llength $shown] == 0} {
if {[Report_gcount $repr $has_ansistring] > $avail} {
set repr [Report_truncate $repr $avail $has_ansistring]
}
lappend shown $repr
set used [Report_gcount $repr $has_ansistring]
continue
}
set rlen [Report_gcount $repr $has_ansistring]
if {$used + 1 + $rlen > $avail} {
break
}
lappend shown $repr
incr used [expr {1 + $rlen}]
}
return [list $shown [llength $shown]]
}
#return number of values we can assign to cater for variable length clauses such as:
# {"elseif" expr "?then?" body}
#review - efficiency? each time we call this - we are looking ahead at the same info
@ -14627,7 +15081,7 @@ package provide punk::args [tcl::namespace::eval punk::args {
tcl::namespace::path {::punk::args::lib ::punk::args::system}
variable pkg punk::args
variable version
set version 0.17.0
set version 0.18.0
}]
return

13
src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.3.0.tm → src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.3.1.tm

@ -6,7 +6,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::args::moduledoc::punkboot 0.3.0
# Application punk::args::moduledoc::punkboot 0.3.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -124,7 +124,14 @@ tcl::namespace::eval punk::args::moduledoc::punkboot {
own -help handling regardless.)
package package to require (guarded) before resolving
argsid - typically a moduledoc-style package
carrying the definition
carrying the definition, or an app-owned docs
package: an execution-gated source of the app's
own file, so the definition lives beside the
parsing code it describes and the help path
never runs the app (the app-punkscript-docs
precedent for the built-in 'script' subcommand -
G-169; see src/lib/app-punkscript/pkgIndex.tcl
for the gate shape)
parse boolean (default 0): validate the subcommand's
arguments through argsid BEFORE the handler runs;
a punk::args usage error (stderr, exit 1) stops
@ -253,6 +260,6 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::args::moduledoc::punkboot [tcl::namespace::eval punk::args::moduledoc::punkboot {
variable version
set version 0.3.0
set version 0.3.1
}]
return

187
src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.6.1.tm → src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.9.0.tm

@ -8,7 +8,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::args::moduledoc::punkexe 0.6.1
# Application punk::args::moduledoc::punkexe 0.9.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::args::moduledoc::punkexe 0 0.6.1]
#[manpage_begin punkshell_module_punk::args::moduledoc::punkexe 0 0.9.0]
#[copyright "2026"]
#[titledesc {punk executable launch documentation}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -33,14 +33,22 @@
#*** !doctools
#[section Overview]
#[para] These definitions are the documentation home for the launch subcommand
#[para] family of the punk executables: tclsh, script, shell, punk, shellspy,
#[para] buildinfo, help (G-118 item 10). The launcher help/parse wiring (G-032)
#[para] renders and parses from them at dispatch time: the boot core's help
#[para] These definitions are the documentation home for the CORE-OWNED part of
#[para] the launch subcommand family of the punk executables: tclsh, shell,
#[para] punk, shellspy, buildinfo, help and the top-level dispatch (G-118 item
#[para] 10). The 'script' subcommand's definition is APP-OWNED (G-169): it
#[para] lives in src/lib/app-punkscript/punkscript.tcl beside the code that
#[para] parses those arguments, provided docs-only as package
#[para] app-punkscript-docs - the ownership rule is docs-live-with-owning-code
#[para] (app-owned for app-handled subcommands, this moduledoc for core-owned
#[para] surfaces). The launcher help/parse wiring (G-032)
#[para] renders and parses from these ids at dispatch time: the boot core's help
#[para] surface clones (script)::punkexe / (script)::punkexe::help per kit with
#[para] the live subcommand set, and per-subcommand ids back the 'help
#[para] <subcommand>' renders and the declared argument gates (the single help
#[para] mechanism - -help flag forms withdrawn 2026-08-05). Id convention follows the
#[para] mechanism - -help flag forms withdrawn 2026-08-05), with app-owned ids
#[para] lazily loaded via their docs package on the help path. Id convention
#[para] follows the
#[para] make.tcl precedent for script-level surfaces: (script)::punkexe and
#[para] (script)::punkexe::<subcommand>, with explicit @form -synopsis overrides
#[para] since a constructed id is not an invocable command name.
@ -196,13 +204,16 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::tclsh
#-formhint suppressed (empty): the ' i -form N ...' default is not typeable
#at the CLI where this renders, and launcher help is subject-only (no
#per-form narrowing to hint at). Interim until such narrowing exists.
#-formhint (G-170, replacing the 0.6.1 interim suppression): the hint
#names real navigation - '<punkexe> help tclsh <formname>' narrows the
#launcher's render to that form (exact form name, or decimal ordinal).
#Deliberately NO trailing ellipsis (the display-hint honesty rule):
#words after the form word are not accepted as a continuation of the
#narrowed render - they flip the tail into the dry-run interpretation.
@cmd -name "punkexe tclsh" -&
-summary -&
"Run the punk executable as a (near) stock tclsh - no punk modules." -&
-formhint "" -&
-formhint " <punkexe> help tclsh %formname%" -&
-help -&
"Run the executable as a plain tclsh: no punk modules are loaded
and behaviour matches stock 'tclsh' as closely as the kit boot
@ -278,6 +289,19 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
interactive form (console stdin, no script) fails fast
with a usage error instead of blocking in a console read."
#G-168 selection-sound forms: scriptfile is fenced against any leading
#'-' word (stock tclMain.c parity - such a word means NO script) and
#the stdin form's FIRST word is fenced against non-dash words (a
#non-dash first word IS a script file) - so form auto-selection
#matches stock classification for every line except the complete
#'-encoding <name> <scriptfile>' shape, which word-shape-matches both
#forms: stock's arity-priority rule (scriptfile wins when the triple
#is complete) is not expressible per-word, so the pair is declared
#@form -overlapallowed and a parse of that shape raises
#multipleformmatches naming both forms (G-041 no-silent-preference;
#the recorded G-168 LIMIT - dispatch itself never parses this
#definition, so the ambiguity surfaces only in declaration-driven
#consumers such as help dry-runs).
@form -form {scriptfile} -synopsis "<punkexe> tclsh ?-encoding name? scriptfile ?arg ...?"
@leaders -min 0 -max 0
@opts
@ -288,16 +312,32 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
-encoding form falls through to the no-script form (stock
behaviour)."
@values -min 1 -max -1
scriptfile -type string -optional 0 -help -&
"Script file to source and exit. ::argv0 and 'info script' are
scriptfile -type string -optional 0 -&
-regexprefail {^-} -&
-regexprefailmsg -&
"a tclsh script file argument may not begin with '-' (stock parity: a line whose script-file position holds a dash-led word is the no-script form - all arguments stay in ::argv)" -&
-help -&
"Script file to source and exit. May not begin with '-' (stock
classification - a dash-led word in this position selects the
no-script form instead). ::argv0 and 'info script' are
the (normalized) script path; remaining args are ::argv. The
script may set ::tclsh(dorepl) 1 (piperepl runtimes) to enter
the interactive repl after it completes."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {stdin} -synopsis "<punkexe> tclsh ?arg ...?"
@form -form {stdin} -synopsis "<punkexe> tclsh ?arg ...?" -overlapallowed {scriptfile}
@values -min 0 -max -1
firstarg -type string -optional 1 -&
-regexprefail {^[^-]} -&
-regexprefailmsg -&
"a non-dash first word after 'tclsh' is a script file (the scriptfile form applies) - the no-script form takes only a leading '-' word here" -&
-help -&
"First argument. In this no-script form it must begin with '-'
(stock classification: a non-dash first word is a script file
and selects the scriptfile form). It is not an option - it
stays in ::argv untouched, exactly like the remaining
arguments."
arg -type any -optional 1 -multiple 1 -help -&
"With no script file all arguments (including any leading '-'
arguments) stay in ::argv; ::argv0 is the executable path.
@ -313,60 +353,20 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
#is the help route). buildinfo below is the dispatch-parsed
#subcommand (exact @values contract enforced through punk::args).
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::script
#-formhint suppressed (empty): as for tclsh above - no CLI 'i', no
#per-form help narrowing. Interim until such narrowing exists.
@cmd -name "punkexe script" -&
-summary -&
"Run a script non-interactively in the punk script environment." -&
-formhint "" -&
-help -&
"Run a Tcl script and exit with an honest exit code
(app-punkscript): script errors exit nonzero and the launch
plumbing emits nothing of its own on stdout/stderr, so
exec-style callers see only the script's output. The default
punk module/alias environment is loaded (unlike 'tclsh').
The script comes from the file argument, from piped stdin when
no argument is given (terminal stdin without a script argument
is a usage error - no interactive fallback), or via scriptlib
resolution when the argument has the form 'lib:name' (resolved
from the scriptlib locations associated with the executable,
including extensionless and shebang'd scripts).
A first argument of exactly '-e' runs the SECOND argument as a
Tcl one-liner (G-077): non-interactive, ::argv0 is '-e',
arguments after the script text land in ::argv, and a
non-empty final result is echoed to stdout (the same one-shot
eval ergonomics as the stdin form). '-e' with no following
script argument is a usage error - never an interactive
fall-through, and never stock tclsh's -e misparse (argv-swallow
plus stdin read). The form works with or without piped stdin
present; stdin is read only if the one-liner itself reads it.
A file literally named '-e' stays reachable as './-e'. The
same one-liner is available at the top level as
'<punkexe> -e <script> ?arg ...?'.
Help flags are ordinary script arguments in every position
(no interception - 2026-08-05 single-mechanism revision):
use '<punkexe> help script' for this usage."
@form -form {file} -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@values -min 0 -max -1
scriptfile -type string -optional 1 -help -&
"Script file path, or lib:name for scriptlib resolution.
Omitted: the script is read from piped stdin."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {oneliner} -synopsis "<punkexe> script -e <script> ?arg ...?"
@values -min 1 -max -1
script -type string -optional 0 -help -&
"Tcl code to evaluate as a one-liner (G-077): errors print
errorInfo to stderr with exit 1; a non-empty final result
is echoed to stdout."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the one-liner in ::argv (::argv0 is '-e')."
}]
#(script)::punkexe::script is NOT defined here (G-169 single-definer
#handover 2026-08-05): the definition lives with the code that parses
#the arguments it describes - src/lib/app-punkscript/punkscript.tcl,
#inertly registered there and provided docs-only as package
#app-punkscript-docs (an execution-gated source of the app's own
#file, so requiring the docs never runs a script). The launcher's
#help path lazily requires that package (punkboot_core.tcl
#launcher_show_help via launcher_builtin_docpackages).
#Ownership rule this records (G-169): docs live with the owning
#code - app-owned for app-handled subcommands (script), moduledoc
#for core-owned surfaces (tclsh and the dispatch-level definitions
#here). The G-168 selection-sound shape travelled with the
#definition unchanged and stays pinned by punkexemodel.test /
#formcheck.test regardless of home.
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::shell
@ -490,14 +490,36 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
with its one-line summary).
With a subcommand argument: that subcommand's own usage.
Words after the subject are accepted and ignored, so 'help'
can simply be prepended to an existing command line
('<punkexe> help tclsh somefile somearg' shows the tclsh
usage). For a project-registered subcommand the rendered
usage is the punk::args definition id its registration
declares; a registration without one gets its registered
summary line (such a subcommand may implement its own -help
handling - the launcher does not touch its arguments).
Words after the subject stay accepted, so 'help' can simply
be prepended to an existing command line - with depth
(G-170):
A SINGLE word exactly naming one of a multi-form
definition's forms (or a decimal form ordinal) narrows the
render to that form ('<punkexe> help script oneliner',
'<punkexe> help tclsh 1'). In this position the form word
wins over argument interpretation; a file literally named
like a form stays dry-runnable via a path spelling
('./oneliner' - the same reachability rule as a file named
'-e', ./-e).
Any other tail on a parse-true subcommand (script, tclsh,
buildinfo, and project registrations declaring parse=1) is
a DRY-RUN of that command line through the subcommand's
definition: an accepted line renders the form it matched
plus a landing report of where each received word landed;
a rejected line emits the same diagnosis dispatch's parse
gate would give, on stderr with exit 1.
Passthrough subcommands (shell, punk, shellspy, and
registrations without parse=1) keep the historic behaviour:
the tail is accepted and ignored ('<punkexe> help shellspy
somearg' shows the shellspy usage). For a project-registered
subcommand the rendered usage is the punk::args definition
id its registration declares; a registration without one
gets its registered summary line (such a subcommand may
implement its own -help handling - the launcher does not
touch its arguments).
Degradation (G-030 doctrine): when punk::args, these
definitions, or the rendering stack are unavailable in the
@ -515,8 +537,15 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
the live launch surface: the built-ins plus any
project-registered subcommands."
arg -type any -optional 1 -multiple 1 -help -&
"Accepted and ignored (reserved): the subject's own command
line may be left in place when prepending 'help'."
"The subject's own command line may be left in place when
prepending 'help' (G-170 depth): a single word naming one
of a multi-form subject's forms (exact name or decimal
ordinal) narrows the render to that form; any other tail
on a parse-true subject (script/tclsh/buildinfo, parse=1
registrations) is dry-run through its definition (accepted:
matched form + landing report; rejected: the dispatch
diagnosis, stderr exit 1); passthrough subjects accept and
ignore the tail as before."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
}
@ -541,7 +570,7 @@ namespace eval ::punk::args::register {
package provide punk::args::moduledoc::punkexe [tcl::namespace::eval punk::args::moduledoc::punkexe {
variable pkg punk::args::moduledoc::punkexe
variable version
set version 0.6.1
set version 0.9.0
}]
return

BIN
src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm

Binary file not shown.

218
src/vfs/_vfscommon.vfs/punkboot/core.tcl

@ -55,7 +55,7 @@
namespace eval ::punkboot {
#boot-core identity (G-031; stamped into kits + reported by the G-025
#buildinfo surfaces)
variable core_version 0.5.0
variable core_version 0.7.0
}
if {![info exists ::punkboot::launch_args]} {
namespace eval ::punkboot [list variable launch_args $::argv]
@ -1529,16 +1529,35 @@ if {![info exists ::punkboot::launch_defaults]} {
#Launcher help + declared-parse machinery (G-032; -help flag forms
#withdrawn 2026-08-05 by user direction - single mechanism)
#The launch surface documents itself through punk::args (definition ids
#from the punk::args::moduledoc::punkexe family) via ONE mechanism, the
#from the punk::args::moduledoc::punkexe family for core-owned surfaces,
#plus app-owned docs packages for app-handled built-ins - G-169: 'script'
#lives in app-punkscript, provided docs-only as app-punkscript-docs and
#lazily required on its help path via launcher_builtin_docpackages) via
#ONE mechanism, the
#leading 'help' word (the punkshell 'i <cmd> ...' idiom):
# <punkexe> help tabled top-level usage; the table
# lists built-ins AND project-registered
# subcommands with summaries
# <punkexe> help <subcommand> ?arg ...?
# the subcommand's tabled usage; words
# after the subject are accepted and
# ignored, so 'help' can be prepended
# to any existing command line
# after the subject stay accepted, so
# 'help' can be prepended to any
# existing command line. G-170 depth:
# a SINGLE word naming one of a
# multi-form definition's forms (exact
# name, or decimal ordinal) narrows the
# render to that form; any other tail
# on a parse-true subcommand (script/
# tclsh/buildinfo, project parse=1
# registrations) is DRY-RUN through
# the definition - accepted lines
# render the matched form plus a
# received-args landing report
# (punk::args::parse_report), rejected
# lines emit the parse diagnosis on
# stderr exit 1; passthrough
# subcommands keep the historic
# ignored-tail render
#There is deliberately NO -help/--help flag handling anywhere in the
#dispatch: a leading help-flag word falls under the unknown-first-argument
#rule (whose refusal shows this usage anyway, stderr exit 1), and
@ -1571,6 +1590,71 @@ if {![info exists ::punkboot::launch_defaults]} {
buildinfo "Print the kit's build/identity stamp report and exit (G-025)." \
help "Show usage for the launch surface or one of its subcommands (G-032)." \
]
#G-169: app-handled built-ins whose punk::args definitions live in
#their app (docs-only package - an execution-gated source of the
#app's own file, so the require never runs the app). Guarded lazy
#require on the help path only - never on boot/dispatch.
variable launcher_builtin_docpackages [dict create \
script app-punkscript-docs \
]
#G-170: built-ins whose definitions are dispatch-faithful, so a help
#tail may be DRY-RUN through them. buildinfo's parity is by
#construction (dispatch parses the same id); script/tclsh are declared
#passthrough surfaces whose G-168 selection-sound definitions MODEL
#dispatch/app reality - their dry-run additionally requires the
#fence-witness probe below (mixed promotion states: an older unfenced
#moduledoc snapshot would misdiagnose lines dispatch accepts).
#shell/punk/shellspy are passthrough with display-only definitions
#(never dry-run); 'help' itself is excluded - its tail handling IS
#this dispatch. Project registrations opt in via parse=1 + argsid
#(parity by construction - dispatch parses the same id).
variable launcher_builtin_parsetrue [list script tclsh buildinfo]
proc launcher_dryrun_capable {subject id} {
#may the help tail dry-run through $id for this subject? 1 when
#accept/reject parity with dispatch is assured for the definition
#actually served in this kit/context (G-170; punkargs_ready is
#assumed probed by the caller).
variable launch_builtin_subcommands
variable launcher_builtin_parsetrue
variable project_subcommand_info
if {$subject in $launch_builtin_subcommands} {
if {$subject ni $launcher_builtin_parsetrue} {
return 0
}
if {$subject eq "buildinfo"} {
#dispatch parses this id itself - any vintage is faithful
return 1
}
#script/tclsh: G-168 fence witness. Selection-soundness rides
#the -regexprefail fences (file-vs-oneliner, scriptfile-vs-
#stdin discrimination); a definition without any fence row is
#a pre-G-168 snapshot whose forms double-match primary lines -
#keep the historic ignored-tail behaviour for it instead of
#misdiagnosing. (A vintage probe, not a soundness proof - the
#fenced definitions are pinned sound by the args-tier
#punkexemodel.test/formcheck.test.)
set capable 0
catch {
set spec [punk::args::get_spec $id]
foreach fid [dict get $spec form_names] {
dict for {argname ainfo} [dict get $spec FORMS $fid ARG_INFO] {
if {[dict exists $ainfo -regexprefail] && [dict get $ainfo -regexprefail] ne ""} {
set capable 1
}
}
}
}
return $capable
}
#project-registered: the declared-parse gate's own keys - dispatch
#parses the same id, so dry-run parity is by construction
if {[dict exists $project_subcommand_info $subject parse]
&& [string is true -strict [dict get $project_subcommand_info $subject parse]]
&& [dict exists $project_subcommand_info $subject argsid]} {
return 1
}
return 0
}
proc launcher_punkargs_ready {} {
#lazy guarded capability probe, one verdict per process. ok requires
#punk::args AND the moduledoc launch definitions to load and resolve
@ -1761,10 +1845,33 @@ if {![info exists ::punkboot::launch_defaults]} {
}
return $ordered
}
proc launcher_show_help {{subject ""}} {
proc launcher_show_help {{subject ""} {tailwords {}}} {
#render help to stdout: the top-level launch surface (empty
#subject) or one subcommand's usage. Tabled via punk::args when
#available; degrades to the plain list/summary. Never errors.
#available; degrades to the plain list/summary. Never errors;
#returns the suggested exit code - 0 for every render, 1 only for
#a rejected dry-run tail (the diagnosis goes to stderr).
#G-170 tail depth (tailwords = the words after the subject):
# - a SINGLE word exactly naming one of a multi-form definition's
# forms (or a decimal ordinal below the form count) narrows the
# render to that form (usage -form; display-only, any vintage).
# Collision rule (G-170 decision): in this position the form
# word wins over dry-run interpretation - a file literally
# named like a form stays dry-runnable via a path spelling
# ('./oneliner'), the same reachability dodge G-168 recorded
# for a file named '-e' (./-e).
# - otherwise, when the subject is dry-run capable
# (launcher_dryrun_capable), the tail parses through the
# definition exactly as make.tcl help does (G-143 precedent):
# an accepted line renders the matched form's usage plus the
# punk::args::parse_report landing report (G-151; guarded -
# older punk::args snapshots simply omit the report); a
# rejected line emits the parse diagnosis on stderr, return 1.
# The complete '-encoding <name> <scriptfile>' tclsh shape
# raises the sanctioned two-form multipleformmatches diagnosis
# (the recorded G-168 LIMIT - honest for that line).
# - passthrough subjects (and degraded contexts) keep the
# historic behaviour: tail accepted and ignored.
variable launch_builtin_subcommands
variable project_subcommand_info
if {[launcher_punkargs_ready]} {
@ -1774,6 +1881,13 @@ if {![info exists ::punkboot::launch_defaults]} {
} elseif {$subject eq "help"} {
set id [launcher_helpdef_id]
} elseif {$subject in $launch_builtin_subcommands} {
#G-169: app-owned built-in docs load lazily here (guarded);
#core-owned ids resolve from the punkexe moduledoc already
#probed by launcher_punkargs_ready
variable launcher_builtin_docpackages
if {[dict exists $launcher_builtin_docpackages $subject]} {
catch {package require [dict get $launcher_builtin_docpackages $subject]}
}
set id (script)::punkexe::$subject
} else {
#project-registered: guarded require of the declared
@ -1787,12 +1901,61 @@ if {![info exists ::punkboot::launch_defaults]} {
#help by registration alone: the summary line
puts stdout "$subject - [launcher_subcommand_summary $subject]"
puts stdout "(no punk::args definition registered for this subcommand - if it is an application entry point it may provide its own -help handling)"
return
return 0
}
}
if {$id ne "" && [llength $tailwords]} {
set form_names [list]
catch {set form_names [punk::args::forms $id]}
#narrowing arm: single form-word on a multi-form definition
if {[llength $tailwords] == 1 && [llength $form_names] > 1} {
set word [lindex $tailwords 0]
set formsel ""
if {$word in $form_names} {
set formsel $word
} elseif {[regexp {^\d+$} $word] && $word < [llength $form_names]} {
set formsel $word
}
if {$formsel ne ""} {
if {![catch {punk::args::usage -form $formsel $id} out]} {
puts stdout $out
return 0
}
#narrowed render failed - fall through to the
#whole-definition render below
}
}
#dry-run arm: parse-true subjects only
if {[launcher_dryrun_capable $subject $id]} {
if {[catch {punk::args::parse $tailwords -errorstyle [launcher_errstyle] withid $id} argd]} {
#the same diagnosis dispatch's parse gate gives
#(project parse=1/buildinfo), or the definition's
#modelled refusal (script/tclsh)
puts stderr $argd
return 1
}
set usage_args [list]
if {[llength $form_names] > 1} {
lappend usage_args -form [dict get $argd form]
}
if {![catch {punk::args::usage {*}$usage_args $id} out]} {
puts stdout $out
#G-151 landing report (Argument|Source|Value);
#guarded: an older punk::args without parse_report
#renders the usage alone
if {[llength [info commands ::punk::args::parse_report]]} {
catch {puts stdout [punk::args::parse_report $argd]}
}
return 0
}
#render failure - fall through to the plain forms
}
#passthrough subject (or unmatched form word on a
#dry-run-incapable subject): historic ignored-tail render
}
if {$id ne "" && ![catch {punk::args::usage $id} out]} {
puts stdout $out
return
return 0
}
#fall through to the plain forms on any render failure
}
@ -1802,7 +1965,7 @@ if {![info exists ::punkboot::launch_defaults]} {
puts stdout "$subject - [launcher_subcommand_summary $subject]"
puts stdout "(plain help: the punk::args tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)"
}
return
return 0
}
proc launcher_unknown_first_error {word} {
#G-032 reclassification rule: an unknown first argument that names
@ -1838,7 +2001,14 @@ if {![info exists ::punkboot::launch_defaults]} {
#arguments). Built-in names cannot be shadowed - a colliding declaration
#is reported and ignored. ::punkboot::project_subcommand_info optionally
#carries per-name help/parse metadata (summary/argsid/package/parse - see
#the launcher help section above, G-032).
#the launcher help section above, G-032). The full key contract is the
#registration definition id (script)::punkboot::registration (the
#punk::args::moduledoc::punkboot durable doc, G-169-recorded location),
#with worked examples in the project_main.tcl template. The 'package' key
#may name an app-owned docs package (an execution-gated source of the
#app's own file - the app-punkscript-docs precedent for the built-in
#'script' subcommand) so a subcommand's documentation lives beside its
#parsing code without the help path ever running the app.
#::punkboot::launch_defaults keys (both optional):
# noargs subcommand assumed for a bare launch (default: shell)
# unknownfirst 'script' treats a non-subcommand first argument as a
@ -2117,14 +2287,21 @@ if {![info exists ::punkboot::launch_defaults]} {
#'help' renders the top-level tabled usage (built-ins +
#project-registered subcommands with summaries);
#'help <subcommand> ?arg ...?' renders that subcommand's usage -
#words after the subject are accepted and ignored, so 'help' can
#be prepended to any existing command line
#('punk9_beta help tclsh somefile somearg' shows the tclsh usage).
#words after the subject stay accepted (prepend-help idiom), with
#G-170 depth: a single form-naming word narrows a multi-form
#definition's render, any other tail on a parse-true subcommand
#dry-runs through its definition (accepted lines render the
#matched form + landing report; rejected lines emit the parse
#diagnosis, stderr exit 1), and passthrough subcommands keep the
#historic ignored-tail render
#('punk9_beta help shellspy somefile somearg' shows the shellspy
#usage) - see launcher_show_help.
#Subject validation parses through the launcherhelp clone when
#punk::args is available (tabled choice errors on stderr exit 1,
#unambiguous prefixes resolve); degraded mode accepts exact names
#only and renders the plain forms. Help goes to stdout, exit 0
#(G-015 output-cleanliness/exit-code doctrine).
#(G-015 output-cleanliness/exit-code doctrine); only a rejected
#dry-run tail exits 1.
if {![llength $subcommand_arglist]} {
::punkboot::launcher_show_help
exit 0
@ -2136,21 +2313,20 @@ if {![info exists ::punkboot::launch_defaults]} {
#precedent), against the required-subject validation clone:
#a mistyped subject earns the tabled choice error (exit 1)
#instead of being skipped past to render the top-level
#table; everything after the subject is deliberately
#unparsed (accepted and ignored - it is the subject's own
#command line, any mix of its words).
#table; the words after the subject go to
#launcher_show_help's G-170 tail handling (narrowing /
#dry-run / historic ignore per subject).
if {[catch {punk::args::parse [lrange $subcommand_arglist 0 0] -errorstyle [::punkboot::launcher_errstyle] withid $helpdef_id} argd]} {
puts stderr $argd
exit 1
}
set help_subject ""
catch {set help_subject [dict get $argd values subject]}
::punkboot::launcher_show_help $help_subject
exit 0
exit [::punkboot::launcher_show_help $help_subject [lrange $subcommand_arglist 1 end]]
}
}
#degraded: exact-name subject only; trailing words ignored as in
#the parsed path
#the historic path (no narrowing/dry-run without punk::args)
set help_subject [lindex $subcommand_arglist 0]
if {$help_subject ni $::punkboot::launch_subcommands} {
puts stderr "help: expected 'help ?subcommand? ?arg ...?' with subcommand one of: [join [::punkboot::launcher_plain_choices] {, }]"

Loading…
Cancel
Save