You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1393 lines
75 KiB
1393 lines
75 KiB
|
|
|
|
#JMN 2021 - Public Domain |
|
#cooperative command renaming |
|
# |
|
# REVIEW 2024 - code was originally for specific use in packageTrace |
|
# - code should be reviewed for more generic utility. |
|
# - API is obscure and undocumented. |
|
# - unclear if intention was only for builtins |
|
# - consider use of newer 'info cmdtype' - (but need also support for safe interps) |
|
# - 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 <cmd> <renamer>' for delegating to command as it was prior to rename |
|
#changes: |
|
#2026-08-04 (G-160 follow-on 2) |
|
# - reload contract: known_renamers and debug are now info-exists guarded |
|
# like the rest of the module state (all_stacks, renamer_command_tokens, |
|
# token_implementations) - previously a module re-source reset both while |
|
# the stacks/tokens survived, stranding live stacks whose renamers were |
|
# no longer known (the removal forms gate on known_renamers membership) |
|
#2026-08-04 (G-160 follow-on) |
|
# - remove_rename convenience forms (the long-parked code todos): |
|
# pop_rename (pop a renamer's topmost entry - per command, or searched |
|
# across live stacks when unambiguous), remove_renamer (remove ALL of a |
|
# renamer's entries across every live stack - the unload-my-package form), |
|
# and restore_original (unwind a command's whole stack to the original |
|
# implementation regardless of renamer; not gated by known_renamers so it |
|
# works after state loss such as a module re-source). All three route |
|
# through remove_rename's re-linking/token-map machinery; stacks parked |
|
# by Rename_stack are invisible to the renamer-wide forms. |
|
#2026-08-03 (hygiene pass - G-160) |
|
# - fix tokenid counter: was incremented on an apply-local variable so it was stuck at 1 - |
|
# tokenids are now unique and monotonic per (renamer,command), so same-renamer re-renames |
|
# dispatch through the full chain and are individually removable by exact token |
|
# - get_next_command resolves tokens via a token->implementation dict (token_implementations) |
|
# instead of scanning the stack list on every dispatch |
|
# - get_IMPLEMENTOR: qualify ::tcl::info::cmdtype in guard and call (the unqualified |
|
# 'info commands' pattern never matched from inside commandstack::util, so the |
|
# 'builtin' classification branch was unreachable) |
|
# - debug validates its argument (clean error for non-boolean) instead of testing the variable |
|
# - informational/warning output is debug-gated (stderr); stray bare 'puts stderr' removed; |
|
# errors remain errors |
|
# - rename records carry a trailing did_rename 0|1 (token/renamer stay 1st/2nd keys - the |
|
# lsearch -index 1 / -index 3 contract is unchanged); no-rename returns are |
|
# {implementation {} did_rename 0} |
|
# - rename_command errors if -renamer appears anywhere but the leading position |
|
# (was silently consumed as a value argument) |
|
# - get_stack tries the raw stacks-dict key before namespace which resolution, so |
|
# Rename_stack-parked stacks are retrievable by key |
|
# - Rename_stack returns 1 (moved) / 0 (no stack) instead of leaking the whole stacks dict |
|
# - Delete_stack errors while the stack still holds live rename records (deleting them |
|
# broke COMMANDSTACKNEXT delegation -> recursion); empty/missing stacks return 1 |
|
# - new delegation helper commandstack::next - equivalent to |
|
# 'uplevel 1 [list $COMMANDSTACKNEXT {*}$args]' from an override body |
|
# - known_renamers defaults reconciled to the vendored packages' actual registration |
|
# strings (packagetrace packagesuppress - were stale ::packagetrace ::packageSuppress) |
|
#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_<mungedcommand> |
|
#2021-09-18 |
|
# - initial version |
|
# - e.g Support cooperation between packageSuppress and packageTrace which both rename the package command |
|
# - They need to be able to load and unload in any order. |
|
# |
|
|
|
#strive for no other package dependencies here. |
|
|
|
|
|
namespace eval commandstack { |
|
#Reload contract: ALL module state survives a re-source (only proc |
|
#definitions refresh) - every state variable here is info-exists guarded. |
|
#known_renamers in particular must survive with all_stacks: stack records |
|
#reference renamer strings and the removal forms gate on known_renamers |
|
#membership, so a reset would strand live stacks. ('variable name <val>' |
|
#is no guard - with a value it re-assigns on every re-source.) |
|
variable all_stacks |
|
variable debug |
|
if {![info exists debug]} { |
|
set debug 0 |
|
} |
|
variable known_renamers |
|
if {![info exists known_renamers]} { |
|
#the strings the vendored cooperating packages actually pass as -renamer |
|
#(reconciled 2026-08-03 - the historical defaults ::packagetrace ::packageSuppress matched no actual registration) |
|
set known_renamers [list packagetrace packagesuppress] |
|
} |
|
if {![info exists all_stacks]} { |
|
#don't wipe it |
|
set all_stacks [dict create] |
|
} |
|
variable renamer_command_tokens |
|
if {![info exists renamer_command_tokens]} { |
|
#monotonically increasing int per {<renamer> <command>} - number of rename_command calls |
|
#that reached the stacking logic (aborted same-body renames consume an id - gaps are fine, |
|
#uniqueness is the contract) |
|
set renamer_command_tokens [dict create] |
|
} |
|
variable token_implementations |
|
if {![info exists token_implementations]} { |
|
#token {<command> <renamer> <tokenid>} -> implementation command. |
|
#Kept in sync with the stack records by rename_command/remove_rename so that |
|
#get_next_command (called on every invocation of every renamed command) is a |
|
#single dict lookup instead of a stack-list scan. |
|
set token_implementations [dict create] |
|
} |
|
} |
|
|
|
namespace eval commandstack::stackdocs { |
|
#Doc blocks attached by 'rename_command -punkargs' for LIVE stack records |
|
#(G-176): PUNKARGS mirrors those records' punk::args definitionlists so a |
|
#punk::args that loads AFTER the renames (early-boot renames precede it) |
|
#picks them up lazily via the ::punk::args::register::NAMESPACES mechanism. |
|
#Maintained by rename_command (attach) and remove_rename (detach - every |
|
#removal path funnels through it). Guarded per the reload contract. |
|
variable PUNKARGS |
|
if {![info exists PUNKARGS]} { |
|
set PUNKARGS [list] |
|
} |
|
} |
|
namespace eval ::punk::args::register { |
|
#inert registration - consumed if/when punk::args loads (idempotent across |
|
#re-source; punk::args need not be present) |
|
variable NAMESPACES |
|
if {![info exists NAMESPACES]} { |
|
set NAMESPACES [list] |
|
} |
|
if {"::commandstack::stackdocs" ni $NAMESPACES} { |
|
lappend NAMESPACES ::commandstack::stackdocs |
|
} |
|
} |
|
|
|
namespace eval commandstack::util { |
|
#note - we can't use something like md5 to ID proc body text because we don't want to require additional packages. |
|
#We could store the full text of the body to compare - but we need to identify magic strings from cooperating packages such as packageTrace |
|
#A magic comment was chosen as the identifying method. |
|
#The string IMPLEMENTOR_*! is searched for where the text between _ and ! is the name of the package that implemented the proc. |
|
|
|
#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_<name>! (which rename_command adds |
|
automatically, keyed by renamer) and returns the <name> |
|
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]]} { |
|
#look for *IMPLEMENTOR_*! |
|
set prefix IMPLEMENTOR_ |
|
set suffix "!" |
|
set body [uplevel 1 [list info body $command]] |
|
if {[string match "*$prefix*$suffix*" $body]} { |
|
set prefixposn [string first "$prefix" $body] |
|
set pkgposn [expr {$prefixposn + [string length $prefix]}] |
|
#set suffixposn [string first $suffix [string range $body $pkgposn $pkgposn+60]] |
|
set suffixposn [string first $suffix $body $pkgposn] |
|
return [string range $body $pkgposn $suffixposn-1] |
|
} else { |
|
return unspecified |
|
} |
|
} else { |
|
#fully qualified guard AND call: 'info commands' pattern namespaces resolve |
|
#relative-only (no global fallback) - the unqualified form never matched from |
|
#inside commandstack::util, making this branch unreachable |
|
if {[info commands ::tcl::info::cmdtype] ne ""} { |
|
#tcl9 and maybe some tcl 8.7s ? |
|
switch -- [::tcl::info::cmdtype $command] { |
|
native { |
|
return builtin |
|
} |
|
default { |
|
return undetermined |
|
} |
|
} |
|
} else { |
|
return undetermined |
|
} |
|
} |
|
} |
|
} |
|
namespace eval commandstack::renamed_commands {} |
|
namespace eval commandstack::temp {} ;#where we create proc initially before renaming into place |
|
|
|
namespace eval commandstack { |
|
namespace export {[a-z]*} |
|
|
|
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 <mypkg> <command> <procargs> <procbody>] |
|
Renames <command> aside and installs <procbody> 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] |
|
or equivalently calls the helper: |
|
commandstack::next {*}$args |
|
The record also carries a trailing did_rename 0|1 verdict. |
|
|
|
set record [commandstack::rename_command -renamer <mypkg> -punkargs <deflists> <command> <procargs> <procbody>] |
|
As above, additionally attaching punk::args doc blocks to the stack |
|
record - each <deflists> element is one definitionlist exactly as |
|
given to punk::args::define (e.g documenting a subcommand the |
|
override adds, with a space-form id such as {::package epoch}). |
|
The docs are live while the record is on the stack and are removed |
|
with it by ANY removal path (remove_rename, pop_rename, |
|
remove_renamer, restore_original). Renames performed BEFORE |
|
punk::args loads are supported: definitions are mirrored in the |
|
registered ::commandstack::stackdocs namespace and load lazily with |
|
punk::args; when punk::args is already loaded they are defined |
|
immediately. Use one live record per doc id - same-id declarations |
|
in two records shadow each other, and removing either removes the |
|
doc. The record carries the attached deflists under a trailing |
|
punkargs key. |
|
|
|
commandstack::remove_rename <token_or_command> |
|
Undo a rename. Accepts the token from the rename record |
|
([dict get $record token] = {<command> <renamer> <tokenid>}), |
|
a 2-element {<command> <renamer>}, or just <command> when called from |
|
the same namespace context that performed the rename. tokenids are |
|
unique per (renamer, command), so any entry - not just the topmost - |
|
is removable by its exact token. |
|
|
|
Convenience removal forms |
|
commandstack::pop_rename <renamer> ?command? |
|
Pop the renamer's topmost entry and return the removed record - |
|
on the given command, or searched across all live stacks when the |
|
renamer's entries live on only one command (multi-command entries |
|
are an ambiguity error). |
|
commandstack::remove_renamer <renamer> |
|
Remove every entry the renamer recorded, across all live stacks - |
|
the unload-my-package form. Returns the removed records keyed by |
|
command. Stacks parked by Rename_stack are left untouched. |
|
commandstack::restore_original <command> |
|
Unwind the command's whole stack regardless of renamer, restoring |
|
the original implementation (a repair/reset operation - not gated |
|
by known_renamers). Returns the number of records removed. |
|
|
|
Inspection |
|
commandstack::get_stack ?command? - rename records (or all stacks; |
|
raw stacks-dict key tried first, |
|
then namespace which resolution) |
|
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 |
|
(informational warnings emit only |
|
when enabled; errors always raise) |
|
|
|
Notes |
|
- Reload contract: a module re-source (re-source / package forget+require |
|
during development) refreshes proc definitions only - ALL module state |
|
survives: all_stacks, renamer_command_tokens, token_implementations, |
|
known_renamers and debug are each info-exists guarded at load, so live |
|
stacks never lose the state that references them. |
|
- The renamer string defaults to the calling namespace. |
|
- Cooperating packages are identified by a magic comment in installed proc |
|
bodies: IMPLEMENTOR_<renamer>! (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::libunknown (the ::package |
|
epoch/forget override), 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; a |
|
non-boolean argument raises an error naming the on_off |
|
argument. When enabled, rename_command and remove_rename |
|
report progress and informational warnings on stderr - with |
|
debug off (the default) they emit nothing (errors are still |
|
raised as errors). The setting survives a module re-source |
|
(the reload contract - see commandstack::help)." |
|
@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 ""} { |
|
return $debug |
|
} |
|
if {![string is boolean -strict $on_off]} { |
|
error "(commandstack::debug) ERROR: on_off argument '$on_off' is not a boolean" |
|
} |
|
set debug [expr {$on_off && 1}] |
|
return $debug |
|
} |
|
|
|
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, returns that command's list of |
|
rename records - empty if the command has never been renamed. |
|
The argument is tried as a raw stacks-dict key first (so |
|
records parked under a non-command key by Rename_stack are |
|
retrievable), then resolved with 'namespace which' in the |
|
caller's context. |
|
Each record is a dict with keys in this order: |
|
token renamer next_implementor next_getter implementation did_rename |
|
The leading key order is a contract: cooperating code may |
|
locate records with 'lsearch -index 1' (token value) or |
|
'lsearch -index 3' (renamer value). Any new keys are appended.} |
|
@values -min 0 -max 1 |
|
command -type string -optional 1 -help -& |
|
"Command name (raw stacks-dict key, else 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 ""} { |
|
return $all_stacks |
|
} |
|
if {[dict exists $all_stacks $command]} { |
|
#raw key match first - also reaches records parked under a |
|
#non-command key by Rename_stack (namespace which cannot resolve those) |
|
return [dict get $all_stacks $command] |
|
} |
|
set resolved [uplevel 1 [list namespace which $command]] |
|
if {$resolved ne "" && [dict exists $all_stacks $resolved]} { |
|
return [dict get $all_stacks $resolved] |
|
} |
|
return [list] |
|
} |
|
|
|
#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. |
|
Resolution is a single dict lookup on the maintained |
|
token->implementation map (no stack scan), so a known token |
|
resolves even while its stack is parked under another key by |
|
Rename_stack. If the token is unknown and the command has no |
|
stack, 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} { |
|
#hot path - called on every invocation of every renamed command (e.g ::package) |
|
#token_implementations is maintained by rename_command/remove_rename so this is |
|
#a single dict lookup rather than an lsearch scan of the stack list |
|
variable token_implementations |
|
if {[dict exists $token_implementations [list $command $renamer $tokenid]]} { |
|
return [dict get $token_implementations [list $command $renamer $tokenid]] |
|
} |
|
variable all_stacks |
|
if {[dict exists $all_stacks $command]} { |
|
error "(commandstack::get_next_command) ERROR: unable to determine next command for '$command' using token: $command $renamer $tokenid" |
|
} |
|
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]] |
|
if {[dict exists $all_stacks $command]} { |
|
set stack [dict get $all_stacks $command] |
|
if {[llength $stack]} { |
|
set rec1 [lindex $stack 0] |
|
tailcall [dict get $rec1 implementation] {*}$args |
|
} else { |
|
tailcall $command {*}$args |
|
} |
|
} else { |
|
tailcall $command {*}$args |
|
} |
|
} |
|
|
|
|
|
#review. |
|
#<renamer> 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] |
|
or equivalently calls the helper: |
|
commandstack::next {*}$args |
|
|
|
Returns the new stack record - a dict with keys: |
|
token renamer next_implementor next_getter implementation did_rename |
|
(plus a trailing punkargs key when -punkargs was given). |
|
The tokenid (third token element) is unique and monotonic per |
|
(renamer, command) pairing, so repeat renames by the same |
|
renamer are individually addressable. When no rename was |
|
performed (command not found, or this renamer already |
|
installed an identical procbody) the returned dict is |
|
{implementation {} did_rename 0} - consumers may test either |
|
key. 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 in the leading option positions (manual parse) - |
|
appearing after the positional arguments begin is an |
|
error." |
|
-punkargs -type list -optional 1 -typesynopsis {list-of-definitionlists} -help -& |
|
"punk::args doc blocks attached to this rename's stack |
|
record - each element is one definitionlist exactly as |
|
given to punk::args::define (e.g documenting a |
|
subcommand the override adds, with a space-form id |
|
such as {::package epoch}). The docs are live while |
|
the record is on the stack and are removed with it by |
|
ANY removal path (remove_rename, pop_rename, |
|
remove_renamer, restore_original). Renames performed |
|
BEFORE punk::args loads are supported: the definitions |
|
are mirrored in the registered |
|
::commandstack::stackdocs namespace and load lazily |
|
with punk::args (keep such deflists free of |
|
dollar-brace tstr substitutions needing your own |
|
namespace context - the lazy load evaluates them in |
|
the stackdocs namespace). |
|
Use one live record per doc id: same-id declarations |
|
in two records shadow each other. Recognised only in |
|
the leading option positions, like -renamer." |
|
@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 |
|
# |
|
set renamer "" |
|
set punkargs_defs [list] |
|
set arglist $args |
|
while {[llength $arglist] > 3 && [string match -* [lindex $arglist 0]]} { |
|
switch -- [lindex $arglist 0] { |
|
-renamer { |
|
set renamer [lindex $arglist 1] |
|
set arglist [lrange $arglist 2 end] |
|
} |
|
-punkargs { |
|
set punkargs_defs [lindex $arglist 1] |
|
set arglist [lrange $arglist 2 end] |
|
} |
|
default { |
|
error "commandstack::rename_command unrecognised leading option '[lindex $arglist 0]'. usage: rename_command ?-renamer <string>? ?-punkargs <list-of-definitionlists>? command procargs procbody" |
|
} |
|
} |
|
} |
|
foreach opt {-renamer -punkargs} { |
|
if {$opt in $arglist} { |
|
error "commandstack::rename_command $opt is recognised only in the leading option positions. usage: rename_command ?-renamer <string>? ?-punkargs <list-of-definitionlists>? command procargs procbody" |
|
} |
|
} |
|
if {[llength $arglist] != 3} { |
|
error "commandstack::rename_command usage: rename_command ?-renamer <string>? ?-punkargs <list-of-definitionlists>? command procargs procbody" |
|
} |
|
lassign $arglist command procargs procbody |
|
|
|
variable debug |
|
set command [uplevel 1 [list namespace which $command]] |
|
if {$command eq ""} { |
|
if {$debug} { |
|
puts stderr "commandstack::rename_command no rename performed for command '[lindex $arglist 0]' by '$renamer'. command not found in calling context. Ensure command name is fully qualified or that command exists." |
|
} |
|
return [dict create implementation "" did_rename 0] |
|
} |
|
variable all_stacks |
|
variable known_renamers |
|
variable renamer_command_tokens |
|
variable token_implementations |
|
if {$renamer eq ""} { |
|
set renamer [uplevel 1 [list namespace current]] |
|
} |
|
if {$renamer ni $known_renamers} { |
|
lappend known_renamers $renamer |
|
} |
|
#unique monotonic tokenid per (renamer,command) - incremented on the REAL namespace |
|
#variable in this proc frame (the historical in-apply 'dict incr' hit an apply-local |
|
#copy, leaving every tokenid at 1 - duplicate tokens broke dispatch and removal for |
|
#same-renamer re-renames). Aborted renames consume an id - gaps are harmless. |
|
dict incr renamer_command_tokens [list $renamer $command] |
|
set tokenid [dict get $renamer_command_tokens [list $renamer $command]] |
|
|
|
#e.g packageTrace and packageSuppress packages use this convention. |
|
set nextinfo [uplevel 1 [list\ |
|
apply {{command renamer procbody tokenid} { |
|
#todo - munge dash so we can make names in renamed_commands separable |
|
# {- _dash_} ? |
|
set mungedcommand [string map {:: _ns_} $command] |
|
set mungedrenamer [string map {:: _ns_} $renamer] |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}-original-$mungedrenamer-$tokenid ;#default is to assume we are the only one playing around with it, but we'll check for known associates too. |
|
set do_rename 0 |
|
if {[llength [info procs $command]] || [llength [info commands $next_target]]} { |
|
#$command is not the standard builtin - something has replaced it, could be ourself. |
|
set next_implementor [::commandstack::util::get_IMPLEMENTOR $command] |
|
set munged_next_implementor [string map {:: _ns_} $next_implementor] |
|
#if undetermined/unspecified it could be the latest renamer on the stack - but we can't know for sure something else didn't rename it. |
|
if {[dict exists $::commandstack::all_stacks $command]} { |
|
set comstacks [dict get $::commandstack::all_stacks $command] |
|
} else { |
|
set comstacks [list] |
|
} |
|
set this_renamer_previous_entries [lsearch -all -index 3 $comstacks $renamer] ;#index 3 is value for second dict entry - (value for key 'renamer') |
|
if {[llength $this_renamer_previous_entries]} { |
|
if {$next_implementor eq $renamer} { |
|
#previous renamer was us. Rather than assume our job is done.. compare the implementations |
|
#don't rename if immediate predecessor is same code. |
|
#set topstack [lindex $comstacks end] |
|
#set next_impl [dict get $topstack implementation] |
|
set current_body [info body $command] |
|
lassign [commandstack::lib::split_body $current_body] _ current_code |
|
set current_code [string trim $current_code] |
|
set new_code [string trim $procbody] |
|
if {$current_code eq $new_code} { |
|
if {$::commandstack::debug} { |
|
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command with same procbody - Aborting rename." |
|
puts stderr [::commandstack::show_stack $command] |
|
} |
|
} else { |
|
if {$::commandstack::debug} { |
|
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command - but appears to be with new code - proceeding." |
|
puts stderr "----------" |
|
puts stderr "$current_code" |
|
puts stderr "----------" |
|
puts stderr "$new_code" |
|
puts stderr "----------" |
|
} |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} |
|
} else { |
|
if {$::commandstack::debug} { |
|
puts stderr "(commandstack::rename_command) WARNING - renamer '$renamer' has already renamed the '$command' command, but is not immediate predecessor - proceeding anyway... (untested)" |
|
} |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} |
|
} elseif {$next_implementor in $::commandstack::known_renamers} { |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}-${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} elseif {$next_implementor in {builtin}} { |
|
#native/builtin could still have been renamed |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} elseif {$next_implementor in {unspecified undetermined}} { |
|
#could be a standard tcl proc, or from application or package |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} else { |
|
if {$::commandstack::debug} { |
|
puts stderr "(commandstack::rename_command) Warning - pkg:'$next_implementor' has renamed the '$command' command. Attempting to cooperate. (untested)" |
|
} |
|
set next_target ::commandstack::renamed_commands::${mungedcommand}_${munged_next_implementor}-$mungedrenamer-$tokenid |
|
set do_rename 1 |
|
} |
|
} else { |
|
#_originalcommand_<mungedcommand> |
|
#assume builtin/original |
|
set next_implementor original |
|
#rename $command $next_target |
|
set do_rename 1 |
|
} |
|
#There are of course other ways in which $command may have been renamed - but we can't detect. |
|
set token [list $command $renamer $tokenid] |
|
return [dict create next_target $next_target next_implementor $next_implementor token $token do_rename $do_rename] |
|
} } $command $renamer $procbody $tokenid] |
|
] |
|
|
|
|
|
if {$debug} { |
|
if {[dict exists $all_stacks $command]} { |
|
set stack [dict get $all_stacks $command] |
|
puts stderr "(commandstack::rename_command) Subsequent rename of command '$command'. (previous renames: [llength $stack]). Renaming to [dict get $nextinfo next_target]" |
|
} else { |
|
#assume this is the original |
|
puts stderr "(commandstack::rename_command) 1st detected rename of command '$command'. Renaming to [dict get $nextinfo next_target]" |
|
} |
|
} |
|
|
|
#token is always first dict entry. (Value needs to be searched with lsearch -index 1 ) |
|
#renamer is always second dict entry (Value needs to be searched with lsearch -index 3) |
|
#additive keys (did_rename) must be APPENDED - the leading key order is a contract. |
|
set new_record [dict create\ |
|
token [dict get $nextinfo token]\ |
|
renamer $renamer\ |
|
next_implementor [dict get $nextinfo next_implementor]\ |
|
next_getter [list ::commandstack::get_next_command {*}[dict get $nextinfo token]]\ |
|
implementation [dict get $nextinfo next_target]\ |
|
did_rename 1\ |
|
] |
|
if {![dict get $nextinfo do_rename]} { |
|
if {$debug} { |
|
puts stderr "commandstack::rename_command no rename performed for command '$command' by '$renamer'" |
|
} |
|
return [dict create implementation "" did_rename 0] |
|
} |
|
if {[llength $punkargs_defs]} { |
|
#additive key - appended after the leading key-order contract keys |
|
#(token idx 0-1, renamer idx 2-3); docs attach only for a rename |
|
#that actually lands (aborted renames returned above) |
|
dict set new_record punkargs $punkargs_defs |
|
} |
|
catch {rename ::commandstack::temp::testproc ""} |
|
set nextinit [string map [list %command% $command %renamer% $renamer %next_getter% [dict get $new_record next_getter] %original_implementation% [dict get $new_record implementation]] { |
|
#IMPLEMENTOR_%renamer%! (mechanism: 'commandstack::rename_command -renamer %renamer% %command% <procargs> <procbody> ) |
|
set COMMANDSTACKNEXT_ORIGINAL %original_implementation% ;#informational/debug for overriding proc. |
|
set COMMANDSTACKNEXT [%next_getter%] |
|
#<commandstack_separator># |
|
}] |
|
set final_procbody "$nextinit$procbody" |
|
#build the proc at a temp location so that if it raises an error we don't adjust the stack or replace the original command |
|
#(e.g due to invalid argument specifiers) |
|
proc ::commandstack::temp::testproc $procargs $final_procbody |
|
uplevel 1 [list rename $command [dict get $nextinfo next_target]] |
|
uplevel 1 [list rename ::commandstack::temp::testproc $command] |
|
dict lappend all_stacks $command $new_record |
|
dict set token_implementations [dict get $nextinfo token] [dict get $nextinfo next_target] |
|
if {[llength $punkargs_defs]} { |
|
Stackdocs_attach $punkargs_defs |
|
} |
|
|
|
return $new_record |
|
} |
|
|
|
#G-176: doc blocks attached to stack records via 'rename_command -punkargs'. |
|
#Attach appends each definitionlist to the commandstack::stackdocs PUNKARGS |
|
#mirror (consumed lazily if punk::args loads later - the namespace is |
|
#registered inert at module load) and defines immediately when punk::args |
|
#is already present (a namespace already consumed into punk::args' |
|
#loaded_packages never lazy-loads later appends; redefining an unchanged id |
|
#is idempotent - a define error surfaces to the rename_command caller with |
|
#the rename already installed, inspectable via show_stack). Detach removes |
|
#ONE mirror occurrence per deflist and undefines via |
|
#punk::args::undefine_deflist (deflist-keyed; silently skips never-defined |
|
#deflists). One live record per doc id is the supported shape - two records |
|
#declaring the same id shadow each other and removing either removes the |
|
#doc. |
|
proc Stackdocs_attach {deflists} { |
|
foreach deflist $deflists { |
|
lappend ::commandstack::stackdocs::PUNKARGS $deflist |
|
if {[llength [info commands ::punk::args::define]]} { |
|
punk::args::define {*}$deflist |
|
} |
|
} |
|
return |
|
} |
|
proc Stackdocs_detach {deflists} { |
|
variable debug |
|
upvar 0 ::commandstack::stackdocs::PUNKARGS docmirror |
|
foreach deflist $deflists { |
|
set posn [lsearch -exact $docmirror $deflist] |
|
if {$posn > -1} { |
|
set docmirror [lreplace $docmirror $posn $posn] |
|
} |
|
if {[llength [info commands ::punk::args::undefine_deflist]]} { |
|
if {[catch {punk::args::undefine_deflist $deflist} errM] && $debug} { |
|
puts stderr "(commandstack::Stackdocs_detach) WARNING: undefine_deflist failed: $errM" |
|
} |
|
} |
|
} |
|
return |
|
} |
|
|
|
namespace eval argdoc { |
|
lappend PUNKARGS [list { |
|
@id -id ::commandstack::next |
|
@cmd -name "commandstack::next" -& |
|
-summary -& |
|
"Delegate from an override body to the next implementation on the stack." -& |
|
-help -& |
|
{Convenience delegation helper for proc bodies installed by |
|
rename_command. Called directly from an override body it |
|
resolves the body's pre-set COMMANDSTACKNEXT variable and |
|
invokes that implementation with the given arguments at the |
|
override's caller level - exactly equivalent to the manual |
|
convention: |
|
uplevel 1 [list $COMMANDSTACKNEXT {*}$args] |
|
Returns the implementation's result. An error is raised when |
|
called from a frame without a COMMANDSTACKNEXT variable (i.e |
|
from anywhere other than directly inside an installed |
|
override body). The COMMANDSTACKNEXT variables remain the |
|
primitive interface - existing consumers need not change.} |
|
@values -min 0 -max -1 |
|
arg -type any -optional 1 -multiple 1 -help -& |
|
"Arguments passed through to the next implementation." |
|
}] |
|
} |
|
proc next {args} { |
|
#call directly from a rename_command-installed override body only: |
|
#COMMANDSTACKNEXT is set at the top of such bodies by the injected header |
|
upvar 1 COMMANDSTACKNEXT COMMANDSTACKNEXT |
|
if {![info exists COMMANDSTACKNEXT]} { |
|
error "(commandstack::next) ERROR: no COMMANDSTACKNEXT variable in the calling frame. commandstack::next must be called directly from a proc body installed by commandstack::rename_command" |
|
} |
|
#uplevel 2 = the override's caller frame - same frame the manual |
|
#'uplevel 1 [list $COMMANDSTACKNEXT {*}$args]' convention evaluates in |
|
uplevel 2 [list $COMMANDSTACKNEXT {*}$args] |
|
} |
|
|
|
#remove by token, or by commandname if called from same context as original rename_command |
|
#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 |
|
lassign $token_or_command command renamer tokenid |
|
} elseif {[llength $token_or_command] == 2} { |
|
#command and renamer only supplied |
|
lassign $token_or_command command renamer |
|
set tokenid "" |
|
} elseif {[llength $token_or_command] == 1} { |
|
#is command name only |
|
set command $token_or_command |
|
set renamer [uplevel 1 [list namespace current]] |
|
set tokenid "" |
|
} |
|
set command [uplevel 1 [list namespace which $command]] |
|
variable all_stacks |
|
variable known_renamers |
|
variable token_implementations |
|
variable debug |
|
if {$renamer ni $known_renamers} { |
|
error "(commandstack::remove_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers' for command '$command'. Ensure remove_rename called from same context as rename_command was, or explicitly supply exact token or {<command> <renamer>}" |
|
} |
|
if {[dict exists $all_stacks $command]} { |
|
set stack [dict get $all_stacks $command] |
|
if {$tokenid ne ""} { |
|
#token_or_command is a token as returned within the rename_command result dictionary |
|
#search first dict value |
|
set doomed_posn [lsearch -index 1 $stack $token_or_command] |
|
} else { |
|
#search second dict value |
|
set matches [lsearch -all -index 3 $stack $renamer] |
|
set doomed_posn [lindex $matches end] ;#we don't have a full token - pop last entry for this renamer |
|
} |
|
if {$doomed_posn ne "" && $doomed_posn > -1} { |
|
set doomed_record [lindex $stack $doomed_posn] |
|
if {[llength $stack] == ($doomed_posn + 1)} { |
|
#last on stack - put the implemenation from the doomed_record back as the actual command |
|
uplevel #0 [list rename $command ""] |
|
uplevel #0 [list rename [dict get $doomed_record implementation] $command] |
|
} elseif {[llength $stack] > ($doomed_posn + 1)} { |
|
#there is at least one more record on the stack - rewrite it to point where the doomed_record pointed |
|
set rewrite_posn [expr {$doomed_posn + 1}] |
|
set rewrite_record [lindex $stack $rewrite_posn] |
|
|
|
if {[dict get $rewrite_record next_implementor] ne $renamer} { |
|
#anomalous stack state (external interference or historical duplicate |
|
#tokens) - conservatively leave the parked implementation in place |
|
if {$debug} { |
|
puts stderr "(commandstack::remove_rename) WARNING: next record on the commandstack didn't record '$renamer' as the next_implementor - not deleting implementation [dict get $rewrite_record implementation]" |
|
} |
|
} else { |
|
uplevel #0 [list rename [dict get $rewrite_record implementation] ""] |
|
} |
|
dict set rewrite_record next_implementor [dict get $doomed_record next_implementor] |
|
#don't update next_getter - it always refers to self |
|
dict set rewrite_record implementation [dict get $doomed_record implementation] |
|
lset stack $rewrite_posn $rewrite_record |
|
dict set all_stacks $command $stack |
|
#re-point the rewritten record's token at its new implementation |
|
dict set token_implementations [dict get $rewrite_record token] [dict get $doomed_record implementation] |
|
} |
|
set stack [lreplace $stack $doomed_posn $doomed_posn] |
|
dict set all_stacks $command $stack |
|
dict unset token_implementations [dict get $doomed_record token] |
|
if {[dict exists $doomed_record punkargs]} { |
|
#G-176: docs attached with 'rename_command -punkargs' die with |
|
#the record. Every removal path (pop_rename/remove_renamer/ |
|
#restore_original) funnels through here. |
|
Stackdocs_detach [dict get $doomed_record punkargs] |
|
} |
|
|
|
} |
|
return $stack |
|
} |
|
return [list] |
|
} |
|
|
|
namespace eval argdoc { |
|
lappend PUNKARGS [list { |
|
@id -id ::commandstack::pop_rename |
|
@cmd -name "commandstack::pop_rename" -& |
|
-summary -& |
|
"Pop a renamer's topmost rename-stack entry." -& |
|
-help -& |
|
{Removes the topmost (most recently stacked) rename entry |
|
belonging to renamer and returns the removed record - the |
|
LIFO undo for a package that renames as it loads and |
|
unwinds as it unloads. With command given, the pop is |
|
restricted to that command's stack (equivalent to |
|
'remove_rename [list $command $renamer]'). Without command, |
|
every live stack is searched: when the renamer's entries |
|
all live on one command the pop happens there; entries |
|
spread across multiple commands are ambiguous and raise an |
|
error naming those commands (supply command, or use |
|
commandstack::remove_renamer to remove all of them). |
|
Stacks parked by Rename_stack are maintenance state and are |
|
not searched. The renamer must be known to commandstack or |
|
an error is raised. Returns the empty string when the |
|
renamer has no matching entry.} |
|
@values -min 1 -max 2 |
|
renamer -type string -help -& |
|
"Renamer string recorded at rename time (must be known |
|
to commandstack)." |
|
command -type string -optional 1 -help -& |
|
"Restrict the pop to this command's stack (resolved in |
|
the caller's namespace context)." |
|
}] |
|
} |
|
proc pop_rename {renamer {command ""}} { |
|
variable all_stacks |
|
variable known_renamers |
|
variable debug |
|
if {$renamer ni $known_renamers} { |
|
error "(commandstack::pop_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time." |
|
} |
|
if {$command ne ""} { |
|
set command [uplevel 1 [list namespace which $command]] |
|
if {$command eq "" || ![dict exists $all_stacks $command]} { |
|
return "" |
|
} |
|
set commands [list $command] |
|
} else { |
|
#find the live stacks holding entries for this renamer. Stacks parked |
|
#by Rename_stack are skipped: their records keep the original token |
|
#command, which never equals the parked key. |
|
set commands [list] |
|
dict for {key stack} $all_stacks { |
|
if {![llength $stack]} { |
|
continue |
|
} |
|
if {[lindex [dict get [lindex $stack 0] token] 0] ne $key} { |
|
continue |
|
} |
|
if {[lsearch -index 3 $stack $renamer] > -1} { |
|
lappend commands $key |
|
} |
|
} |
|
if {[llength $commands] > 1} { |
|
error "(commandstack::pop_rename) ERROR: renamer '$renamer' has entries on multiple commands ([join $commands {, }]) - supply the command argument, or use commandstack::remove_renamer to remove all of its entries" |
|
} |
|
if {![llength $commands]} { |
|
return "" |
|
} |
|
} |
|
set command [lindex $commands 0] |
|
set stack [dict get $all_stacks $command] |
|
set topmost [lindex [lsearch -all -index 3 $stack $renamer] end] |
|
if {$topmost eq ""} { |
|
return "" |
|
} |
|
set record [lindex $stack $topmost] |
|
remove_rename [list $command $renamer] |
|
if {$debug} { |
|
puts stderr "(commandstack::pop_rename) popped [dict get $record token]" |
|
} |
|
return $record |
|
} |
|
|
|
namespace eval argdoc { |
|
lappend PUNKARGS [list { |
|
@id -id ::commandstack::remove_renamer |
|
@cmd -name "commandstack::remove_renamer" -& |
|
-summary -& |
|
"Remove every rename-stack entry belonging to a renamer." -& |
|
-help -& |
|
{The unload-my-package convenience: removes ALL entries |
|
recorded for renamer across every live command stack. Each |
|
command's entries are popped topmost-first through the same |
|
re-linking machinery as remove_rename, so other renamers' |
|
overrides keep delegating correctly. Stacks parked by |
|
Rename_stack are maintenance state and are left untouched. |
|
The renamer must be known to commandstack or an error is |
|
raised; the renamer is NOT removed from known_renamers. |
|
Returns a dict keyed by command name whose values are the |
|
removed records (topmost-first); an empty dict when the |
|
renamer had no entries.} |
|
@values -min 1 -max 1 |
|
renamer -type string -help -& |
|
"Renamer string recorded at rename time (must be known |
|
to commandstack)." |
|
}] |
|
} |
|
proc remove_renamer {renamer} { |
|
variable all_stacks |
|
variable known_renamers |
|
variable debug |
|
if {$renamer ni $known_renamers} { |
|
error "(commandstack::remove_renamer) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time." |
|
} |
|
set removed [dict create] |
|
#remove_rename mutates all_stacks only under the command key it is |
|
#given, so iterating over this snapshot of the stacks dict is safe. |
|
#Stacks parked by Rename_stack are skipped: their records keep the |
|
#original token command, which never equals the parked key. |
|
dict for {command stack} $all_stacks { |
|
if {![llength $stack]} { |
|
continue |
|
} |
|
if {[lindex [dict get [lindex $stack 0] token] 0] ne $command} { |
|
continue |
|
} |
|
while {[set topmost [lindex [lsearch -all -index 3 [dict get $all_stacks $command] $renamer] end]] ne ""} { |
|
set record [lindex [dict get $all_stacks $command] $topmost] |
|
remove_rename [list $command $renamer] |
|
dict lappend removed $command $record |
|
if {$debug} { |
|
puts stderr "(commandstack::remove_renamer) removed [dict get $record token]" |
|
} |
|
} |
|
} |
|
return $removed |
|
} |
|
|
|
namespace eval argdoc { |
|
lappend PUNKARGS [list { |
|
@id -id ::commandstack::restore_original |
|
@cmd -name "commandstack::restore_original" -& |
|
-summary -& |
|
"Unwind a command's whole rename stack, restoring the original implementation." -& |
|
-help -& |
|
{Removes EVERY record on the command's live rename stack - |
|
regardless of which renamers contributed them - and |
|
restores the bottom-of-stack (original) implementation as |
|
the live command. Records are removed topmost-first through |
|
the same machinery as remove_rename. |
|
This is the repair/reset operation: it is deliberately NOT |
|
gated by known_renamers (unlike remove_rename) - renamers |
|
evidenced by the stack records are registered into |
|
known_renamers first, mirroring rename_command, so a |
|
restore still works after state loss such as known_renamers |
|
being cleared while stacks survived. |
|
Returns the number of records removed: 0 when the command |
|
has no live stack (never renamed, or only the empty residue |
|
entry - prune that with Delete_stack).} |
|
@values -min 1 -max 1 |
|
command -type string -help -& |
|
"Command name (resolved with 'namespace which' in the |
|
caller's context)." |
|
}] |
|
} |
|
proc restore_original {command} { |
|
variable all_stacks |
|
variable known_renamers |
|
variable debug |
|
set command [uplevel 1 [list namespace which $command]] |
|
if {$command eq "" || ![dict exists $all_stacks $command]} { |
|
return 0 |
|
} |
|
set stack [dict get $all_stacks $command] |
|
if {![llength $stack]} { |
|
return 0 |
|
} |
|
#a repair operation must not be gated by known_renamers (which state |
|
#surgery may have lost while stacks survived) - register the renamers |
|
#the stack evidences, mirroring rename_command |
|
foreach record $stack { |
|
set record_renamer [dict get $record renamer] |
|
if {$record_renamer ni $known_renamers} { |
|
lappend known_renamers $record_renamer |
|
} |
|
} |
|
set removed_count 0 |
|
while {[llength $stack]} { |
|
remove_rename [dict get [lindex $stack end] token] |
|
set stack [dict get $all_stacks $command] |
|
incr removed_count |
|
} |
|
if {$debug} { |
|
puts stderr "(commandstack::restore_original) restored '$command' to its original implementation ($removed_count override(s) unwound)" |
|
} |
|
return $removed_count |
|
} |
|
|
|
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]} { |
|
#if caller is attempting exact match - use the calling context to resolve in case they didn't supply namespace |
|
set commandname_glob [uplevel 1 [list namespace which $commandname_glob]] |
|
} |
|
if {[package provide punk::lib] ne "" && [package provide punk] ne ""} { |
|
#punk pipeline also needed for patterns |
|
return [punk::lib::pdict -channel none all_stacks $commandname_glob/@*/@*.@*] |
|
} else { |
|
set result "" |
|
set matchedkeys [dict keys $all_stacks $commandname_glob] |
|
#don't try to calculate widest on empty list |
|
if {[llength $matchedkeys]} { |
|
set widest [tcl::mathfunc::max {*}[lmap v $matchedkeys {tcl::string::length $v}]] |
|
set indent [string repeat " " [expr {$widest + 3}]] |
|
set indent2 "${indent} " ;#8 spaces for " i = " where i is 4 wide |
|
set padkey [string repeat " " 20] |
|
foreach k $matchedkeys { |
|
append result "$k = " |
|
set i 0 |
|
foreach stackmember [dict get $all_stacks $k] { |
|
if {$i > 0} { |
|
append result "\n$indent" |
|
} |
|
append result [string range "$i " 0 4] " = " |
|
set j 0 |
|
dict for {k v} $stackmember { |
|
if {$j > 0} { |
|
append result "\n$indent2" |
|
} |
|
set displaykey [string range "$k$padkey" 0 20] |
|
append result "$displaykey = $v" |
|
incr j |
|
} |
|
incr i |
|
} |
|
append result \n |
|
} |
|
} |
|
return $result |
|
} |
|
} |
|
|
|
#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. An error |
|
is raised when the stack still holds rename records - live |
|
overrides installed by rename_command must be removed with |
|
remove_rename first (deleting the records of a live override |
|
historically broke the COMMANDSTACKNEXT lookup so the next |
|
call recursed to the interp limit). An empty stack entry |
|
(the residue after the last remove_rename) is deleted and 1 |
|
is returned; a command with no stack entry also returns 1. |
|
Not exported - intended for maintenance/experimentation only |
|
(under review).} |
|
@values -min 1 -max 1 |
|
command -type string -help -& |
|
"Stacks-dict key - fully qualified command name or parked |
|
stack name (no resolution is performed)." |
|
}] |
|
} |
|
proc Delete_stack {command} { |
|
variable all_stacks |
|
if {[dict exists $all_stacks $command]} { |
|
set stack [dict get $all_stacks $command] |
|
if {[llength $stack]} { |
|
#records represent live renames - deleting them would break the |
|
#COMMANDSTACKNEXT delegation of the installed overrides (the next |
|
#call would recurse to the interp limit) |
|
error "(commandstack::Delete_stack) ERROR: stack for '$command' still holds [llength $stack] live rename record(s) - remove them with commandstack::remove_rename first" |
|
} |
|
dict unset all_stacks $command |
|
return 1 |
|
} else { |
|
return 1 |
|
} |
|
} |
|
|
|
#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). Returns 1 when a stack was moved, |
|
0 when oldname has no stack. An error is raised if newname |
|
already has a stack. get_stack tries its argument as a raw |
|
stacks-dict key first, so parked records are retrievable by |
|
the parked name; COMMANDSTACKNEXT delegation of live |
|
overrides keeps working while parked (token resolution uses |
|
the token->implementation map, not the stacks-dict key). 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]} { |
|
return 0 |
|
} |
|
if {[dict exists $all_stacks $newname]} { |
|
error "(commandstack::rename_stack) cannot rename $oldname to $newname - $newname already exists in stack" |
|
} |
|
dict set all_stacks $newname [dict get $all_stacks $oldname] |
|
dict unset all_stacks $oldname |
|
return 1 |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
if {[string length $str] == 0} { |
|
return {} |
|
} |
|
if {[string length $regexp] == 0} { |
|
return [::split $str ""] |
|
} |
|
if {[regexp $regexp {}]} { |
|
return -code error "splitting on regexp \"$regexp\" would cause infinite loop" |
|
} |
|
|
|
set list {} |
|
set start 0 |
|
while {[regexp -start $start -indices -- $regexp $str match submatch]} { |
|
foreach {subStart subEnd} $submatch break |
|
foreach {matchStart matchEnd} $match break |
|
incr matchStart -1 |
|
incr matchEnd |
|
lappend list [string range $str $start $matchStart] |
|
if {$subStart >= $start} { |
|
lappend list [string range $str $subStart $subEnd] |
|
} |
|
set start $matchEnd |
|
} |
|
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 #<commandstack_separator># 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 <command>')." |
|
}] |
|
} |
|
proc split_body {procbody} { |
|
set marker "#<commandstack_separator>#" |
|
set header "" |
|
set code "" |
|
set found_marker 0 |
|
foreach ln [split $procbody \n] { |
|
if {!$found_marker} { |
|
if {[string trim $ln] eq $marker} { |
|
set found_marker 1 |
|
} else { |
|
append header $ln \n |
|
} |
|
} else { |
|
append code $ln \n |
|
} |
|
} |
|
if {$found_marker} { |
|
return [list $header $code] |
|
} else { |
|
return [list "" $procbody] |
|
} |
|
} |
|
} |
|
|
|
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 0.8.0 |
|
}] |
|
|
|
|
|
|