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.
 
 
 
 
 
 

275 lines
9.9 KiB

#!/usr/bin/env tclsh
# nslist.tcl - runtime namespace/command discovery report, aimed at agent/LLM use
#
# Answers "what does this namespace/package actually expose?" - the discovery
# counterpart of whatis.tcl (which answers "how do I call this command?").
# Reports for each namespace matched by <nsglob>:
# - child namespaces, marking which are also loadable package names
# - commands classified by kind (procs, aliases, ensembles, native, imported,
# oo classes/objects, coroutines, interps, zlibstreams)
# - which commands have punk::args documentation ('documented:' line) - those
# give full usage detail via whatis.tcl / punk::ns::cmdhelp
# - namespace export patterns and 'namespace path' entries
# Data comes from punk::ns::get_ns_dicts - the same runtime machinery behind the
# interactive n// and n/// namespace browsers - so runtime-generated commands are
# included and only what is actually loaded is reported.
# If the target namespace does not exist, 'package require' of the name (then of
# successively shorter :: prefixes) is attempted first to populate it.
# An ensemble command name (e.g 'string') is followed to its implementation
# namespace, with a note.
# Options:
# -synopsis append punk::args usage lines for each documented command
# -pathcommands also list the commands resolvable via each 'namespace path' entry
#
# Must run inside a punkshell interpreter (needs punk::ns, punk::args):
# punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>...
# Use the 'src' launch argument so the working-tree modules under src/modules are
# loaded and queried rather than the kit-stamped copies inside the executable.
# <nsglob> may be a plain namespace (punk::ns - listed as ::punk::ns::*) or a
# glob: last-segment glob chars match members, earlier-segment glob chars match
# multiple namespaces, and a trailing ::** segment recurses through all
# descendant namespaces.
# Exit 0 if every pattern produced a listing; 1 if any pattern matched nothing;
# 2 usage error.
if {[catch {package require punk::ns} errmsg]} {
puts stderr "nslist: requires a punkshell interpreter (package require punk::ns failed: $errmsg)"
exit 1
}
catch {package require punk::args}
proc strip {text} {
if {[info commands ::ansistrip] ne ""} {
return [ansistrip $text]
} elseif {![catch {package require punk::ansi}]} {
return [punk::ansi::ansistrip $text]
}
return $text
}
proc out {key val} {
puts [format "%-16s %s" $key: [strip $val]]
}
# attempt to load a package matching $name, else successively shorter :: prefixes
proc try_autoload {name} {
set parts [lsearch -all -inline -not [split [string trimleft $name :] ::] {}]
for {set n [llength $parts]} {$n >= 1} {incr n -1} {
set prefix [join [lrange $parts 0 $n-1] ::]
if {![catch {package require $prefix}]} {
return $prefix
}
}
return {}
}
# leading non-glob namespace of a fully-qualified (possibly glob) path e.g ::a::b* -> ::a
proc nonglob_base {fq} {
set parts [lsearch -all -inline -not [split [string trimleft $fq :] ::] {}]
set keep [list]
foreach seg $parts {
if {[regexp {[*?]} $seg]} {
break
}
lappend keep $seg
}
return ::[join $keep ::]
}
proc emit_nsdict {nd opt_pathcommands opt_synopsis} {
set location [dict get $nd location]
out namespace $location
foreach {label key} {
children children
packagetails packagetails
packageprefixes packageprefixes
} {
set items [dict get $nd $key]
if {[llength $items]} {
out $label [lsort -dictionary $items]
}
}
set commands [dict get $nd commands]
out commands [llength $commands]
set classified [list]
foreach {label key} {
procs procs
aliases aliases
ensembles ensembles
native native
imported imported
ooclasses ooclasses
ooobjects ooobjects
ooprivateclasses ooprivateclasses
ooprivateobjects ooprivateobjects
coroutines coroutines
interps interps
zlibstreams zlibstreams
undetermined undetermined
} {
set items [dict get $nd $key]
if {[llength $items]} {
out $label [lsort -dictionary $items]
lappend classified {*}$items
}
}
set other [list]
foreach c $commands {
if {$c ni $classified} {
lappend other $c
}
}
if {[llength $other]} {
out other [lsort -dictionary $other]
}
set exported [dict get $nd exported]
if {[llength $exported]} {
out exported [lsort -dictionary $exported]
}
set documented [lsort -dictionary [dict get $nd usageinfo]]
if {[llength $documented]} {
out documented $documented
}
set exportpatterns [dict get $nd namespacexport]
if {[llength $exportpatterns]} {
out exportpatterns $exportpatterns
}
set pathdict [dict get $nd namespacepath]
if {[dict size $pathdict]} {
out namespacepath [dict keys $pathdict]
if {$opt_pathcommands} {
dict for {p pinfo} $pathdict {
set pcmds [list]
if {[dict exists $pinfo commands]} {
set pcmds [dict get $pinfo commands]
}
puts [format " pathcommands(%s): %s" $p [strip [lsort -dictionary $pcmds]]]
}
}
}
if {[dict get $nd itemcount] == 0} {
out note "(no members matched glob '[dict get $nd glob]' - namespace empty, or the package populating it is not loaded)"
}
if {$opt_synopsis && [llength $documented]} {
puts "synopsis:"
foreach c $documented {
set id [punk::ns::nsjoin $location $c]
if {![punk::args::id_exists $id]} {
# alias/import - resolve the documentation id the way cmdhelp does
set id ""
if {![catch {punk::ns::cmdinfo [punk::ns::nsjoin $location $c]} cinfo]} {
set docid [dict get $cinfo docid]
if {$docid ne "" && [punk::args::id_exists $docid]} {
set id $docid
}
}
}
set syn ""
if {$id ne "" && ![catch {punk::args::synopsis $id} s]} {
set syn [string trim [strip $s]]
}
if {$syn eq ""} {
puts [format " %s (no punk::args synopsis available)" $c]
} else {
foreach line [split $syn \n] {
puts " $line"
}
}
}
}
}
# ---------------------------------------------------------------- arg handling
set opt_synopsis 0
set opt_pathcommands 0
set globs [list]
foreach a $argv {
switch -- $a {
-synopsis {set opt_synopsis 1}
-pathcommands {set opt_pathcommands 1}
default {lappend globs $a}
}
}
if {![llength $globs]} {
puts stderr "usage: punk91 src script scriptlib/developer/nslist.tcl ?-synopsis? ?-pathcommands? <nsglob>..."
puts stderr " e.g: punk91 src script scriptlib/developer/nslist.tcl punk::ns"
exit 2
}
# ---------------------------------------------------------------- main
set failures 0
set output_started 0
foreach g $globs {
set fq [string map {:::: ::} $g]
if {![string match ::* $fq]} {
set fq ::[string trimleft $fq :]
}
set notes [list]
set base [nonglob_base $fq]
if {![namespace exists $base]} {
set loaded [try_autoload $base]
if {$loaded ne "" && [namespace exists $base]} {
lappend notes "loaded package '$loaded [package present $loaded]' to populate $base"
}
}
set query ""
if {![regexp {[*?]} $fq]} {
if {[namespace exists $fq]} {
set query ${fq}::*
if {[namespace ensemble exists $fq]} {
lappend notes "$fq is also an ensemble command - whatis.tcl shows its subcommand map"
}
} else {
# not a namespace - maybe a command (follow an ensemble to its namespace)
set resolved [uplevel #0 [list namespace which -command $fq]]
if {$resolved ne "" && [namespace ensemble exists $resolved]} {
set ensns [namespace ensemble configure $resolved -namespace]
lappend notes "'$g' is an ensemble command implemented in namespace $ensns - listing that (whatis.tcl shows the authoritative subcommand map)"
set query ${ensns}::*
} elseif {$resolved ne ""} {
puts stderr "nslist: '$g' is a command, not a namespace - use the tcl-whatis skill: punk91 src script scriptlib/developer/whatis.tcl [string trimleft $g :]"
incr failures
continue
} else {
puts stderr "nslist: no namespace (or command) '$g' found - and no package matching it could be loaded"
incr failures
continue
}
}
} else {
if {[namespace exists $base]} {
set query $fq
} else {
puts stderr "nslist: base namespace $base of glob '$g' not found - and no package matching it could be loaded"
incr failures
continue
}
}
if {[catch {punk::ns::get_ns_dicts $query -nspathcommands $opt_pathcommands} dicts]} {
puts stderr "nslist: punk::ns::get_ns_dicts failed for '$query': $dicts"
incr failures
continue
}
if {![llength $dicts]} {
puts stderr "nslist: no namespaces matched '$query'"
incr failures
continue
}
if {$output_started} {
puts ""
}
set output_started 1
foreach n $notes {
out note $n
}
set i 0
foreach nd $dicts {
if {$i > 0} {
puts ""
}
incr i
emit_nsdict $nd $opt_pathcommands $opt_synopsis
}
}
exit [expr {$failures == 0 ? 0 : 1}]