diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0448a7..0dd90dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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.49.2] - 2026-08-03 + +- commandstack 0.5.0: `commandstack::help` now returns a real API overview + (was an empty string), and every API proc carries a PUNKARGS documentation + block registered lazily with punk::args (no punk::args dependency added - + the module remains dependency-free). First test suite added: + `src/tests/modules/commandstack/` characterises the rename/remove/stack + behaviour the in-tree consumers (punk::packagepreference, punk::nav::fs, + punk auto_execok, packagetrace, packagesuppress) rely on, and pins known + defects as _GAP_ tests (tokenid stuck at 1 causing duplicate-token dispatch + bypass, unreachable get_IMPLEMENTOR 'builtin' branch, Delete_stack + live-rename recursion). + ## [0.49.1] - 2026-08-03 - punk::libunknown 0.2.4: deep tm discovery (`register_all_tm`, used by diff --git a/punkproject.toml b/punkproject.toml index 0e35c292..fc563988 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.49.1" +version = "0.49.2" license = "BSD-2-Clause" url = "https://www.gitea1.intx.com.au/jn/punkshell" #packager: declared identity for published artifacts (declarative, not proof - diff --git a/src/modules/commandstack-999999.0a1.0.tm b/src/modules/commandstack-999999.0a1.0.tm index 95b07c95..c025c7ca 100644 --- a/src/modules/commandstack-999999.0a1.0.tm +++ b/src/modules/commandstack-999999.0a1.0.tm @@ -11,6 +11,9 @@ # - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack. # - document that replacement command should use 'commandstack::get_next_command ' for delegating to command as it was prior to rename #changes: +#2026-08-03 +# - implement commandstack::help overview text (was returning empty string) +# - add PUNKARGS documentation blocks for the API (lazy punk::args registration - no punk::args dependency added) #2024 # - mungecommand to support namespaced commands # - fix mistake - hardcoded _originalcommand_package -> _originalcommand_ @@ -43,6 +46,25 @@ namespace eval commandstack::util { #return unspecified if the command is a proc with a body but no magic comment ID #return unknown if the command doesn't have a proc body to analyze #otherwise return the package name identified in the magic comment + namespace eval ::commandstack::argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::util::get_IMPLEMENTOR + @cmd -name "commandstack::util::get_IMPLEMENTOR" -& + -summary -& + "Identify which package implemented a command's current proc body." -& + -help -& + {Searches the proc body of command for the magic comment + marker IMPLEMENTOR_! (which rename_command adds + automatically, keyed by renamer) and returns the + portion. Returns 'unspecified' for a proc body without the + marker, 'builtin' for a native command when + tcl::info::cmdtype is available (Tcl 8.7+/9), otherwise + 'undetermined'.} + @values -min 1 -max 1 + command -type string -help -& + "Command name - must already be fully qualified." + }] + } proc get_IMPLEMENTOR {command} { #assert - command has already been resolved to a namespace ie fully qualified if {[llength [info procs $command]]} { @@ -81,12 +103,90 @@ namespace eval commandstack::temp {} ;#where we create proc initially before ren namespace eval commandstack { namespace export {[a-z]*} - proc help {} { - return { - } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::help + @cmd -name "commandstack::help" -& + -summary -& + "Plain-text overview of the commandstack module." -& + -help -& + "Returns a dependency-free plain text overview of the + cooperative command renaming system: the core + rename_command/remove_rename workflow, the COMMANDSTACKNEXT + delegation variables injected into installed proc bodies, + and the inspection commands." + @values -min 0 -max 0 + }] + } + proc help {} { + return {commandstack - cooperative command renaming (stacked command overrides) + + Purpose + Allows multiple packages to override the same command (e.g the ::package + builtin) and to load/unload their overrides in any order. Each override is + recorded on a per-command stack so that removing one re-links the + surrounding entries instead of clobbering them. + + Core workflow + set record [commandstack::rename_command -renamer ] + Renames aside and installs in its place. + The previous implementation is preserved at the command name given by + [dict get $record implementation] (empty string means no rename was + performed). Two variables are pre-set at the top of the installed + proc body: + COMMANDSTACKNEXT - the implementation to delegate to, + re-resolved on every call via + commandstack::get_next_command (so it + stays correct when the stack changes) + COMMANDSTACKNEXT_ORIGINAL - the implementation as at rename time + (static - informational/debug) + A delegating body normally contains: + uplevel 1 [list $COMMANDSTACKNEXT {*}$args] + + commandstack::remove_rename + Undo a rename. Accepts the token from the rename record + ([dict get $record token] = { }), + a 2-element { }, or just when called from + the same namespace context that performed the rename. + + Inspection + commandstack::get_stack ?command? - rename records (or all stacks) + commandstack::show_stack ?glob? - printable stack display + commandstack::basecall command ?arg ...? - call bottom-of-stack (original) + commandstack::get_next_command command renamer tokenid + - implementation a record points to + commandstack::debug ?on_off? - query/set debug messages + + Notes + - The renamer string defaults to the calling namespace. + - Cooperating packages are identified by a magic comment in installed proc + bodies: IMPLEMENTOR_! (added automatically by rename_command). + - Per-command detail is registered lazily with punk::args - e.g + `i commandstack::rename_command` in punkshell, or + `punk::args::usage ::commandstack::rename_command` when punk::args is loaded. + + In-tree users: punk::packagepreference, punk::nav::fs, punk (auto_execok), + packagetrace, packagesuppress. +} } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::debug + @cmd -name "commandstack::debug" -& + -summary -& + "Query or set commandstack debug messaging." -& + -help -& + "With no argument, returns the current debug state (0|1). + With a boolean argument, sets the state and returns it. + When enabled, rename_command reports rename progress on + stderr." + @values -min 0 -max 1 + on_off -type boolean -optional 1 -help -& + "New debug state. Omit to query the current state." + }] + } proc debug {{on_off {}}} { variable debug if {$on_off eq ""} { @@ -99,6 +199,30 @@ namespace eval commandstack { } } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::get_stack + @cmd -name "commandstack::get_stack" -& + -summary -& + "Return the rename-record stack for a command, or all stacks." -& + -help -& + {With no argument, returns the entire stacks dict keyed by + fully qualified command name - each value a list of rename + records (bottom of stack first). + With a command argument (resolved with 'namespace which' in + the caller's context), returns that command's list of rename + records - empty if the command has never been renamed. + Each record is a dict with keys in this order: + token renamer next_implementor next_getter implementation + The key order is a contract: cooperating code may locate + records with 'lsearch -index 1' (token value) or + 'lsearch -index 3' (renamer value).} + @values -min 0 -max 1 + command -type string -optional 1 -help -& + "Command name (resolved in the caller's namespace context). + Omit to return the dict of all stacks." + }] + } proc get_stack {{command ""}} { variable all_stacks if {$command eq ""} { @@ -115,6 +239,30 @@ namespace eval commandstack { #get the implementation to which the renamer (renamer is usually calling namespace) originally renamed it, or the implementation it now points to. #review - performance impact. Possible to use oo for faster dispatch whilst allowing stack re-orgs? #e.g if renaming builtin 'package' - this command is generally called 'a lot' + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::get_next_command + @cmd -name "commandstack::get_next_command" -& + -summary -& + "Resolve the implementation a rename record delegates to." -& + -help -& + {Returns the implementation command to which the stack entry + identified by the token elements (command renamer tokenid) + currently points. Installed override bodies call this on + every invocation (via the pre-set COMMANDSTACKNEXT variable), + so removals from the stack re-route delegation automatically. + If the command has no stack at all, command is returned + unchanged. An error is raised when a stack exists but no + record matches the token.} + @values -min 3 -max 3 + command -type string -help -& + "Fully qualified command name (first token element)." + renamer -type string -help -& + "Renamer string recorded at rename time (second token element)." + tokenid -type int -help -& + "Token id recorded at rename time (third token element)." + }] + } proc get_next_command {command renamer tokenid} { variable all_stacks if {[dict exists $all_stacks $command]} { @@ -131,6 +279,25 @@ namespace eval commandstack { return $command } } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::basecall + @cmd -name "commandstack::basecall" -& + -summary -& + "Call the original (bottom-of-stack) implementation of a command." -& + -help -& + {Tailcalls the implementation recorded at the bottom of the + command's rename stack (the original command as it was first + renamed aside), bypassing all stacked overrides. A command + with no rename stack is called directly. The command name is + resolved with 'namespace which' in the caller's context.} + @values -min 1 -max -1 + command -type string -help -& + "Command name (resolved in the caller's namespace context)." + arg -type any -optional 1 -multiple 1 -help -& + "Arguments passed through to the implementation." + }] + } proc basecall {command args} { variable all_stacks set command [uplevel 1 [list namespace which $command]] @@ -150,6 +317,57 @@ namespace eval commandstack { #review. # defaults to calling namespace - but can be arbitrary string + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::rename_command + @cmd -name "commandstack::rename_command" -& + -summary -& + "Cooperatively rename a command, stacking the override." -& + -help -& + {Renames command aside (to a name under + ::commandstack::renamed_commands) and installs a proc with + procargs/procbody in its place, recording the operation on + the command's rename stack so overrides from multiple + cooperating packages can be added and removed in any order. + + A header is prepended to procbody which sets two variables: + COMMANDSTACKNEXT - the implementation to delegate + to (re-resolved every call via + commandstack::get_next_command) + COMMANDSTACKNEXT_ORIGINAL - the implementation as at rename + time (static/debug) + A delegating procbody normally contains: + uplevel 1 [list $COMMANDSTACKNEXT {*}$args] + + Returns the new stack record - a dict with keys: + token renamer next_implementor next_getter implementation + An implementation value of empty string means no rename was + performed (command not found, or this renamer already + installed an identical procbody). Keep the token + ({command renamer tokenid}) or the {command renamer} pair for + a later remove_rename. + + The proc is first built at a temp location so a procargs or + procbody compile error raises before the stack or the live + command are touched.} + @opts + -renamer -type string -optional 1 -help -& + "Identity string recorded for this rename - defaults to + the calling namespace. Cooperating packages use their + package/namespace name. Note: this flag is recognised + only as the FIRST argument (manual parse)." + @values -min 3 -max 3 + command -type string -help -& + "Command to rename (resolved with 'namespace which' in + the caller's context - builtins and procs both work)." + procargs -type list -help -& + "Argument list for the replacement proc (commonly {args}, + but any signature matching the target's call pattern)." + procbody -type string -help -& + "Body for the replacement proc. Delegate onward via the + pre-set COMMANDSTACKNEXT variable." + }] + } proc rename_command {args} { #todo: consider -forcebase 1 or similar to allow this rename to point to bottom of stack (original command) bypassing existing renames # - need to consider that upon removing, that any remaining rename that was higher on the stack should not also be diverted to the base - but rather to the next lower in the stack @@ -322,6 +540,37 @@ namespace eval commandstack { #If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed. #A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything. #similarly a nonexistant token or renamer will not remove anything and will just return the current stack + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::remove_rename + @cmd -name "commandstack::remove_rename" -& + -summary -& + "Undo a rename previously made with rename_command." -& + -help -& + {Removes one entry from a command's rename stack, restoring + or re-linking implementations as needed. Entries other than + the topmost can be removed - the entry above is re-pointed at + what the removed entry delegated to (the load/unload-in-any- + order design goal). + + token_or_command is one of: + 3 elements - the exact token from the rename record: + {command renamer tokenid} + 2 elements - {command renamer} - removes that renamer's + topmost entry for the command + 1 element - command name only - renamer defaults to the + calling namespace + The renamer must be known to commandstack (recorded by a + rename_command call) or an error is raised. A token or + renamer with no matching stack entry removes nothing. + Returns the command's stack after the removal (empty list if + the command has no stack).} + @values -min 1 -max 1 + token_or_command -type list -help -& + "Token {command renamer tokenid}, pair {command renamer}, + or bare command name (see -help above)." + }] + } proc remove_rename {token_or_command} { if {[llength $token_or_command] == 3} { #is token @@ -384,6 +633,26 @@ namespace eval commandstack { return [list] } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::show_stack + @cmd -name "commandstack::show_stack" -& + -summary -& + "Return a printable display of rename stacks." -& + -help -& + {Returns a formatted text display of the rename stacks whose + command names match commandname_glob. An argument without + glob characters is resolved with 'namespace which' in the + caller's context first. When the punk and punk::lib packages + are already loaded the display is rendered with + punk::lib::pdict - otherwise a plain aligned-text fallback is + used. Returns an empty string when nothing matches.} + @values -min 0 -max 1 + commandname_glob -type string -default * -optional 1 -help -& + "Glob pattern (or exact command name) selecting which + command stacks to display." + }] + } proc show_stack {{commandname_glob *}} { variable all_stacks if {![regexp {[?*]} $commandname_glob]} { @@ -430,6 +699,27 @@ namespace eval commandstack { #review #document when this is to be called. Wiping stacks without undoing renames seems odd. + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::Delete_stack + @cmd -name "commandstack::Delete_stack" -& + -summary -& + "Discard a command's rename-stack records (maintenance - unexported)." -& + -help -& + {Removes the command's entry from the stacks dict WITHOUT + undoing any renames - the renamed commands themselves are + left in place. Always returns 1, whether or not a stack + existed. CAUTION: if overrides installed by rename_command + are still live, deleting their stack breaks the + COMMANDSTACKNEXT lookup - get_next_command then resolves to + the (overridden) command itself and the next call recurses + until the interp recursion limit. Not exported - intended for + maintenance/experimentation only (under review).} + @values -min 1 -max 1 + command -type string -help -& + "Fully qualified command name key (no resolution is performed)." + }] + } proc Delete_stack {command} { variable all_stacks if {[dict exists $all_stacks $command]} { @@ -442,6 +732,29 @@ namespace eval commandstack { #can be used to temporarily put a stack aside - should manually rename back when done. #review - document how/when to use. example? intention? + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::Rename_stack + @cmd -name "commandstack::Rename_stack" -& + -summary -& + "Re-key a command's rename-stack records (maintenance - unexported)." -& + -help -& + {Moves the stack records stored under oldname to newname in + the stacks dict. No commands are renamed - this only changes + the dict key, e.g to temporarily put a stack aside (rename + back manually when done). An error is raised if newname + already has a stack. Note that get_stack resolves its + argument with 'namespace which', so records parked under a + name that is not an existing command are only visible via the + no-argument get_stack dict. Not exported - intended for + maintenance/experimentation only (under review).} + @values -min 2 -max 2 + oldname -type string -help -& + "Existing stacks-dict key (no resolution is performed)." + newname -type string -help -& + "New stacks-dict key." + }] + } proc Rename_stack {oldname newname} { variable all_stacks if {[dict exists $all_stacks $oldname]} { @@ -465,6 +778,26 @@ namespace eval commandstack { namespace eval commandstack::lib { + namespace eval ::commandstack::argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::lib::splitx + @cmd -name "commandstack::lib::splitx" -& + -summary -& + "Split a string on a regexp separator." -& + -help -& + {Local copy of tcllib textutil::split::splitx (to avoid the + dependency). Splits str on each match of regexp. A + parenthesised subexpression in regexp includes the separator + match in the result list. An empty regexp splits into + characters. A regexp matching the empty string raises an + 'infinite loop' error.} + @values -min 1 -max 2 + str -type string -help -& + "String to split." + regexp -type string -optional 1 -default {[\t \r\n]+} -help -& + "Separator regular expression." + }] + } proc splitx {str {regexp {[\t \r\n]+}}} { #snarfed from tcllib textutil::splitx to avoid the dependency # Bugfix 476988 @@ -494,6 +827,23 @@ namespace eval commandstack::lib { lappend list [string range $str $start end] return $list } + namespace eval ::commandstack::argdoc { + lappend PUNKARGS [list { + @id -id ::commandstack::lib::split_body + @cmd -name "commandstack::lib::split_body" -& + -summary -& + "Split an installed override body into commandstack header and original code." -& + -help -& + {Splits a proc body at the ## marker + line that rename_command embeds between its generated header + (the COMMANDSTACKNEXT setup) and the renamer-supplied + procbody. Returns a 2-element list {header code}. A body + without the marker returns {"" procbody}.} + @values -min 1 -max 1 + procbody -type string -help -& + "Proc body text (e.g from 'info body ')." + }] + } proc split_body {procbody} { set marker "##" set header "" @@ -518,6 +868,14 @@ namespace eval commandstack::lib { } } +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + #Register namespaces punk::args should scan for PUNKARGS documentation. + #The PUNKARGS metadata here is inert documentation - this module deliberately + #does not depend on (or call) punk::args. + lappend ::punk::args::register::NAMESPACES ::commandstack ::commandstack::argdoc +} + package provide commandstack [namespace eval commandstack { set version 999999.0a1.0 }] diff --git a/src/modules/commandstack-buildversion.txt b/src/modules/commandstack-buildversion.txt index 635e16ae..00541ab5 100644 --- a/src/modules/commandstack-buildversion.txt +++ b/src/modules/commandstack-buildversion.txt @@ -1,3 +1,7 @@ -0.4.1 -#First line must be a tm version number -#all other lines are ignored. \ No newline at end of file +0.5.0 +#First line must be a tm version number +#all other lines are ignored. +#0.5.0 - commandstack::help now returns a real overview (was empty string) +# - PUNKARGS documentation blocks added for all API procs (lazy punk::args +# registration via ::punk::args::register::NAMESPACES - no punk::args +# dependency added; module remains dependency-free) diff --git a/src/tests/AGENTS.md b/src/tests/AGENTS.md index 1d1a5b83..58d84ccb 100644 --- a/src/tests/AGENTS.md +++ b/src/tests/AGENTS.md @@ -30,6 +30,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc - Test files must `package require` any extra packages explicitly. - Tcltest files must finish with `tcltest::cleanupTests`; missing cleanup produces a `missing-cleanupTests` runner warning and only untrusted observed testcase events. - tcltest compares the `-body` RETURN VALUE against `-result`. The suite convention of accumulating into `$result` via `lappend` works because `lappend` returns the list — but a body whose last command is a loop (`foreach`, `while`) returns the empty string; end such bodies with an explicit `set result`. +- Test DESCRIPTIONS must be a single content line (long lines are fine; the corpus style is `test name {one long description}\` with the options on following lines). tcltest prints a failing test's opening banner as one `puts` of `==== FAILED`, and tcltest only trims the description's ENDS — a description with embedded newlines makes that banner multi-line, punk::tcltestrun's per-line output parser then never matches the opening banner, misreads the closing `==== FAILED` line as an opener, and stays in its failure-capture state to end of stream: every later event INCLUDING the summary line is swallowed, so the file reports `warn`/`missing-cleanupTests` with observed passes stopping at the failure and no failure detail at all (discovered 2026-08-03 authoring the commandstack suite). Put longer prose in comments above the test. - Agent-oriented runner output should use `-report compact -show-passes 0` for focused checks unless detailed Markdown pass lists are needed. - `-report json` emits a machine-readable final summary, but package-load warnings may still precede it on stdout/stderr, and the punk ANSI output stack may emit an SGR reset immediately before the JSON on the same line (`scriptlib/developer/runtests_parity.tcl` tolerates both). - Human-facing pass/fail/warning indicators in markdown/compact reports are ANSI-coloured only when stdout is a real windows console (`-colour auto` default: twapi `GetConsoleMode` on the STD_OUTPUT handle as the isatty-equivalent - `get_console_handle` is unsuitable, it succeeds for piped children of console shells; `NO_COLOR` honoured; `-colour on|off` overrides). Machine-facing output (the `RUNTESTS_RESULT` line, json reports) is never coloured, and piped/redirected output stays plain for agents. The runner uses raw literal SGR rather than `a+`/`a` so its indicators are independent of punk::console's process-global colour state. diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index c73cbe40..8fd29238 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -40,6 +40,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src