## -*- tcl -*-
# # ## ### ##### ######## ############# #####################
## CMDR - Config - Collection of argument values for a private.
# @@ Meta Begin
# Package cmdr::config 0
# Meta author {Andreas Kupries}
# Meta location https://core.tcl.tk/akupries/cmdr
# Meta platform tcl
# Meta summary Internal. Collection of parameters for cmd::private instances.
# Meta description Internal. Collection of parameters for cmd::private instances.
# Meta subject {command line}
# Meta require TclOO
# Meta require cmdr::help
# Meta require cmdr::parameter
# Meta require cmdr::util
# Meta require cmdr::validate
# Meta require debug
# Meta require debug::caller
# Meta require linenoise::facade
# Meta require term::ansi::code::ctrl
# Meta require try
# Meta require {Tcl 8.5-}
# Meta require {oo::util 1.2}
# Meta require {struct::queue 1}
# @@ Meta End
## - The config manages the argument values, and can parse
## a command line against the definition, filling values,
## issuing errors on mismatches, etc.
# # ## ### ##### ######## ############# #####################
## Requisites
package require Tcl 8.5
package require debug
package require debug::caller
package require linenoise::facade
package require struct::queue 1 ; #
package require term::ansi::code::ctrl
package require try
package require TclOO
package require oo::util 1.2 ; # link helper
package require cmdr::help
package require cmdr::parameter ; # Parameter to collect
package require cmdr::util
package require cmdr::validate ; # Core validation types.
# # ## ### ##### ######## ############# #####################
debug define cmdr/config
debug level cmdr/config
debug prefix cmdr/config {[debug caller] | }
# # ## ### ##### ######## ############# #####################
## Definition
oo::class create ::cmdr::config {
# # ## ### ##### ######## #############
classmethod interactive {{value 1}} {
variable ourinteractive $value
return
}
classmethod display {cmdprefix} {
variable ourdisplay $cmdprefix
return
}
method display {{plist {}}} {
if {![llength $plist]} {
set plist [my Visible]
}
set plist [lsort -dict $plist]
return [{*}$mydisplay $plist]
}
# # ## ### ##### ######## #############
## Lifecycle.
forward context context
constructor {context spec} {
debug.cmdr/config {}
classvariable ourinteractive
if {![info exists ourinteractive]} { set ourinteractive 0 }
classvariable ourdisplay
if {[info exists ourdisplay]} {
set mydisplay $ourdisplay
} else {
set mydisplay [mymethod PrintState]
}
my Colors
# Import the context (cmdr::private).
interp alias {} [self namespace]::context {} $context
# Initialize collection state.
set myinteractive $ourinteractive
set mynames {} ;# list of parameter names
set mymap {} ;# parameter name -> object
set mypub {} ;# parameter name -> object, non-state only, i.e. user visible
set myoption {} ;# option -> object
set myfullopt {} ;# option prefix -> list of full options having that prefix.
set myargs {} ;# List of argument names.
set myinforce no
# Import the DSL commands.
link \
{undocumented Undocumented} \
{description Description} \
{use Use} \
{input Input} \
{interactive Interactive} \
{option Option} \
{state State}
# Updated in my DefineParameter, called from the $spec
set splat no
# Auto inherit common options, state, arguments.
# May not be defined.
catch { use *all* }
eval $spec
# Postprocessing
my SetThresholds
my UniquePrefixes
my CompletionGraph
set mypq [struct::queue P] ;# actual parameters
if {[llength $myargs]} {
set myaq [struct::queue A] ;# formal argument parameters
}
return
}
method help {{mode public}} {
debug.cmdr/config {}
# command = dict ('desc' -> description
# 'options' -> options
# 'arguments' -> arguments
# 'parameters' -> parameters)
# options = list (name -> desc) // name -> index into parameters
# arguments = list (argument-name...) // name -> index into parameters
# code in {
# + <=> required
# ? <=> optional
# +* <=> required splat
# ?* <=> optional splat
# }
# parameters = dict (name -> param-def)
# param-def = dict (key -> value) // include code
#
# Option aliases are listed in options, but not in parameters.
set options {}
dict for {o para} $myoption {
# in interactive mode undocumented options can be shown in
# the help if they already have a value defined for them.
if {![$para documented] &&
(($mode ne "interact") ||
![$para defined?])} continue
# in interactive mode we skip all the aliases.
if {($mode eq "interact") &&
![$para primary $o]} continue
dict set options $o [$para description $o]
}
set arguments $myargs
# Full dump of the parameter definitions. Unusual formats
# (SQL, json) may wish to have acess to all of a parameter,
# not just bits and pieces.
foreach p $mynames {
set para [dict get $mymap $p]
dict set parameters $p [$para help]
}
return [dict create \
desc [context description] \
options $options \
arguments $arguments \
parameters $parameters]
}
method interactive {} { return $myinteractive }
method eoptions {} { return $myfullopt }
method names {} { return [dict keys $mymap] }
method public {} { return [dict keys $mypub] }
method arguments {} { return $myargs }
method options {} { return [dict keys $myoption] }
method lookup {name} {
debug.cmdr/config {}
if {![dict exists $mymap $name]} {
set names [linsert [join [lsort -dict [my names]] {, }] end-1 or]
return -code error -errorcode {CMDR CONFIG PARAMETER UNKNOWN} \
"Got \"$name\", expected parameter name, one of $names"
}
return [dict get $mymap $name]
}
method lookup-option {name} {
debug.cmdr/config {}
if {![dict exists $myoption $name]} {
set names [linsert [join [lsort -dict [my options]] {, }] end-1 or]
return -code error -errorcode {CMDR CONFIG PARAMETER UNKNOWN} \
"Got \"$name\", expected option name, one of $names"
}
return [dict get $myoption $name]
}
method force {{allowinteract yes} {forcedefered no}} {
debug.cmdr/config {}
my Force $allowinteract $forcedefered
return
}
method Force {allowinteract forcedefered} {
debug.cmdr/config {recursive=$myinforce}
# Define the values of all parameters.
# Done in order of declaration.
# Any dependencies between parameter can be handled by proper
# declaration order.
if {$myinforce} return
set myinforce yes
foreach name $mynames {
set para [dict get $mymap $name]
# Ignore parameters which defer value generation until
# actual use, except if we are forced to compute them.
if {!$forcedefered && [$para defered]} continue
if {!$allowinteract} {
$para dontinteract
}
try {
$para value
} trap {CMDR PARAMETER UNDEFINED} {e o} {
# Ignore when a parameter was not defined.
# Note that this is transparent to validation
# errors.
}
}
set myinforce no
return
}
method reset {} {
debug.cmdr/config {}
dict for {name para} $mymap {
$para reset
}
return
}
method forget {} {
debug.cmdr/config {}
if {$myinforce} return
dict for {name para} $mymap {
$para forget
}
return
}
# # ## ### ##### ######## #############
## API for use by the actual command run by the private, and by
## the values in the config (which may request other values for
## their validation, generation, etc.). Access to argument values by name.
method unknown {m args} {
debug.cmdr/config {}
if {![regexp {^@(.*)$} $m -> mraw]} {
# Standard error message when not @name ...
next $m {*}$args
return
}
# @name ... => handlerof(name) ...
if {![llength $args]} { lappend args value }
return [[my lookup $mraw] {*}$args]
}
# # ## ### ##### ######## #############
method SetThresholds {} {
debug.cmdr/config {}
# Compute the threshold needed by optional arguments to decide
# when they can take an argument.
# The threshold is the number of actual parameters required to
# satisfy all _required_ arguments coming after the current
# argument. Computed from back to front, starting with 0 (none
# required after the last argument), this value increments for
# each required argument found. Optional arguments do not count.
set required 0
#set rlist {} ; # Debugging aid
foreach a [lreverse $myargs] {
set para [dict get $mymap $a]
$para threshold: $required
#lappend rlist $required
if {[$para required]} {
incr required
}
}
# Debug, show mapping.
#puts A|$myargs|
#puts T|[lreverse $rlist]|
return
}
method UniquePrefixes {} {
debug.cmdr/config {}
dict for {k v} $myoption {
set prefix ""
foreach c [split $k {}] {
append prefix $c
if {$prefix in {- --}} continue
if {[dict exists $myoption $prefix]} {
dict set myfullopt $prefix [list $k]
} else {
dict lappend myfullopt $prefix $k
}
}
}
# Sort the expansions, for the error messages.
dict for {k v} $myfullopt {
if {[llength $v] == 1} continue
dict set myfullopt $k [lsort -dict $v]
}
#array set _o $myoption ; parray _o ; unset _o
#array set _f $myfullopt ; parray _f ; unset _f
return
}
method CompletionGraph {} {
debug.cmdr/config {}
set next {}
set start .(start)
set end .(end)
# Basic graph, linear chain of the arguments
foreach from [linsert $myargs 0 $start] to [linsert $myargs end $end] {
dict lappend next $from $to
# Loop the chain for a list argument.
if {($from ne $start) && [[dict get $mymap $from] list]} {
dict lappend next $from $from
}
} ; #my SCG $start $next chain
# Extend the graph, adding links bypassing the optional
# arguments. Essentially an iterative transitive closure
# where the epsilon links are only implied.
set changed 1
set handled {} ;# Track processed epsilon links to not follow
# them again.
while {$changed} {
set changed 0
foreach a [linsert $myargs 0 $start] {
foreach n [dict get $next $a] {
if {$n eq $end} continue
if {[[dict get $mymap $n] required]} continue
if {[dict exists $handled $a,$n]} continue
# make sucessors of a sucessor optional argument my sucessors, once
dict set handled $a,$n .
set changed 1
foreach c [dict get $next $n] {
dict lappend next $a $c
}
}
}
} ; #my SCG $start $next closure
# Convert the graph into a list of states, i.e. sets of
# arguments (note that the underlying structure is still
# essentially linear, which the DFA from the NFA now exposes
# again).
set mycchain {}
foreach a [linsert $myargs 0 $start] {
# Tweaks: Ensure state uniqueness, and a canoninical order.
set state [lsort -unique [lsort -dict [dict get $next $a]]]
# Remove the end state
set pos [lsearch -exact $state $end]
if {$pos >= 0} { set state [lreplace $state $pos $pos] }
# Loop state, list argument last.
if {([llength $state] == 1) && $a eq [lindex $state 0]} {
set state ... ; # marker for stepper in complete-words.
}
lappend mycchain $state
}
#puts stderr \t[join $mycchain \n\t]
return
}
method SCG {start next label} {
puts stderr \n/$label
foreach a [linsert $myargs 0 $start] {
puts stderr "\t($a) => [dict get $next $a]"
}
return
}
# # ## ### ##### ######## #############
## API for cmdr::private parameter specification DSL.
# Description is for the context, i.e. the private.
forward Description context description:
forward Undocumented context undocumented
# Bespoke 'source' command for common specification fragments.
method Use {name} {
debug.cmdr/config {}
# Pull code fragment out of the data store and run.
uplevel 1 [context get $name]
return
}
method Interactive {} {
debug.cmdr/config {}
set myinteractive 1
return
}
# Parameter definition itself.
# order, cmdline, required, defered (O C R D) name ?spec?
forward Input my DefineParameter 1 1 1 0
forward Option my DefineParameter 0 1 0 0
forward State my DefineParameter 0 0 1 1
# O+C+R specify the parameter type. D may vary.
method DefineParameter {
order cmdline required defered
name desc {spec {}}
} {
debug.cmdr/config {}
upvar 1 splat splat
if {$splat && $order} {
return -code error -errorcode {CMDR CONFIG SPLAT ORDER} \
"A splat must be the last argument in the specification"
}
my ValidateAsUnknown $name
# Create and initialize handler.
set para [cmdr::parameter create param_$name [self] \
$order $cmdline $required $defered \
$name $desc $spec]
# Map parameter name to handler object.
dict set mymap $name $para
# And a second map, user-visible parameters only,
# i.e. available on the cmdline, and documented.
if {[$para cmdline] && [$para documented]} {
dict set mypub $name $para
}
if {$order} {
# Arguments, keep names, in order of definition
lappend myargs $name
set splat [$para list]
} else {
# Keep map of options to their handlers.
foreach option [$para options] {
dict set myoption $option $para
}
}
# And the list of all parameters in declaration order, for use
# in 'force'.
lappend mynames $name
return
}
method ValidateAsUnknown {name} {
debug.cmdr/config {}
if {![dict exists $mymap $name]} return
return -code error -errorcode {CMDR CONFIG KNOWN} \
"Duplicate parameter \"[context fullname]: $name\", already specified."
}
# # ## ### ##### ######## #############
## Command completion. This is the entry point for recursion from
## the higher level officers, delegated to config from cmdr::private
## Note that command completion for the REPL of the private is
## handled by the internal cmdr::config instance, which also manages
## the REPL itself.
method complete-words {parse} {
debug.cmdr/config {} 10
dict with parse {}
# -> ok, at, nwords, words, line
#puts ?|$ok
#puts @|$at
#puts #|$nwords
#puts =|$words|
#puts L|$line|
# The basic idea is to scan over the words, like with 'parse',
# except that instead of letting the parameters taking their
# values we keep track of which parameters could have been
# set. To avoid complexities here we use the mycchain computed
# by CompletionGraph to know the set of possible parameters,
# simply stepping through.
# at = word in the command line we are at.
# ac = state in the completion chain we are at.
# st = processing state
set ac 0 ;# parameters which can be expected at this position
set st none ;# expect an argument word
set current [lindex $words $at end]
while {$at < ($nwords-1)} {
if {$st eq "eov"} {
# Skip over the option value
set st none
incr at
continue
}
# We need just the text of the current word.
set current [lindex $words $at end]
if {[my IsOption $current implied]} {
if {!$implied} {
# Expect next word to be an option value.
set st eov
}
} else {
# Step to the chain state for the next word.
# Note how we bounce back on the loop/list marker.
incr ac
if {[lindex $mycchain $ac] eq "..."} { incr ac -1 }
}
# Step to the next word
incr at
}
# assert (at == (nwords-1))
# We are now on the last word, and the system state tells us
# what we can expect in terms of parameters and such.
set state [lindex $mycchain $ac]
dict set parse at $at
#puts '|$current|
#puts @|$at|
#puts c|$ac|
#puts x|$state|
#puts s|$st|
if {$st eq "eov"} {
# The last word is an option value, possible incomplete.
# The value of 'current' still points to the option name.
# Determine the responsible parameter, and delegate.
# Unknown option, unable to complete the value.
if {![dict exists $myfullopt $current]} { return {} }
# Ambiguous option name, unable to complete value.
set matches [dict get $myfullopt $current]
if {[llength $matches] > 1} { return {} }
# Delegate to the now known parameter for completion.
set match [lindex $matches 0]
set para [dict get $myoption $match]
return [$para complete-words $parse]
}
# Not at option value, can be at incomplete option name, and parameters.
set current [lindex $words $at end]
if {$current eq {}} {
# All options are possible here.
set completions [my options]
# And the completeable values of the possible arguments.
foreach a $state {
lappend completions {*}[[dict get $mymap $a] complete-words $parse]
}
return $completions
}
if {[string match -* $current]} {
# Can be option name, or value, if implied (special form --foo=bar).
if {[set pos [string first = $current]] < 0} {
# Just option name to complete.
return [context match $parse [my options]]
} else {
set prefix [string range $current 0 $pos]
set option [string range $prefix 0 end-1] ;# chop =
# Unknown option, unable to complete the value.
if {![dict exists $myfullopt $option]} { return {} }
# Ambiguous option name, unable to complete value.
set matches [dict get $myfullopt $option]
if {[llength $matches] > 1} { return {} }
# Delegate to the now known parameter for completion.
set match [lindex $matches 0]
set para [dict get $myoption $match]
incr pos
set val [string range $curent $pos end]
dict lappend parse words $val
dict incr parse at
set completions
foreach c [$para complete-words $parse] {
lappend completions $prefix$c
}
return $completions
}
}
# Only the completeable values of the possible arguments.
set completions {}
foreach a $state {
lappend completions {*}[[dict get $mymap $a] complete-words $parse]
}
return $completions
}
# # ## ### ##### ######## #############
method IsOption {current iv} {
debug.cmdr/config {} 10
upvar 1 $iv implied at at nwords nwords words words
set implied 0
if {![string match -* $current]} {
# Cannot be option
return 0
}
# Is an option (even if not known).
if {[string first = $current] >= 0} {
# --foo=bar special form.
set implied 1
} else {
# Try to expand the flag and look the whole option up. If
# we can, check if it is boolean, and if yes, look at the
# next argument, if any to determine if it belongs to the
# option, or not. The latter then means the argument is
# implied.
set next [expr {$at+1}]
if {$next < $nwords} {
# Have a following word
if {[dict exists $myfullopt $current]} {
# Option is possibly known
set matches [dict get $myfullopt $current]
if {[llength $matches] == 1} {
# Option is unambiguously known
set match [lindex $matches 0]
set para [dict get $myoption $match]
if {[$para isbool]} {
# option is boolean
set next [lindex $words $next end]
if {![string is boolean $next]} {
# next word is not boolean => value is implied.
# note that we are non-strict here.
# an empty word is treated as boolean to be completed, not implied.
set implied 1
}
}
}
}
}
}
return 1
}
# # ## ### ##### ######## #############
## API for cmdr::private use of the arguments.
## Runtime parsing of a command line, parameter extraction.
method parse-options {} {
debug.cmdr/config {}
# The P queue contains a mix of options and arguments. An
# optional argument was encountered and has called on this to
# now process all options so that it can decode wether to take
# the front value for itself or not. The front value is
# definitely not an option.
# Nothing to process.
if {![P size]} {
debug.cmdr/config {no parameters to process}
return
}
# Unshift the front value under consideration by
# 'cmdr::parameter Take'.
lappend arguments [P get]
# Process the remainder for options and their values.
while {[P size]} {
set word [P peek]
if {[string match -* $word]} {
my ProcessOption
continue
}
lappend arguments [P get]
}
# Refill the queue with the arguments which remained after
# option processing.
if {![llength $arguments]} {
debug.cmdr/config {no arguments to return}
return
}
P put {*}$arguments
debug.cmdr/config {done}
return
}
method parse {args} {
debug.cmdr/config {}
# - Reset the state values (we might be in an interactive shell, multiple commands).
# - Stash the parameters into a queue for processing.
# - Stash the (ordered) arguments into a second queue.
# - Operate on parameter and arg queues until empty,
# dispatching the words to handlers as needed.
my reset
P clear
if {[llength $args]} { P put {*}$args }
if {![llength $myargs]} {
debug.cmdr/config {options only}
# The command has no arguments. It may accept options.
while {[P size]} {
set word [P peek]
debug.cmdr/config {[P size] ? $word}
if {![string match -* $word]} {
# Error. No regular arguments to accept.
my tooMany
}
my ProcessOption
}
return
}
# Process commands and flags, in order.
A clear
A put {*}$myargs
debug.cmdr/config {a[A size] p[P size]}
while {1} {
debug.cmdr/config {a|[expr {[A size] ? [A peek [A size]] : ""}]|}
debug.cmdr/config {p|[expr {[P size] ? [P peek [P size]] : ""}]|}
# Option ... Leaves A unchanged.
if {[P size]} {
set word [P peek]
debug.cmdr/config {[P size] ? $word}
if {[string match -* $word]} {
try {
my ProcessOption
} trap {CMDR CONFIG BAD OPTION} {e o} {
# Test if we have regular arguments left, and
# if the first of them is willing to accept
# the word. If yes, the bad option is treated
# as regular argument.
if {![A size] ||
![[dict get $mymap [A peek]] accept $word]} {
return {*}$o $e
}
debug.cmdr/config {as argument}
P unget $word
my ProcessArgument
}
continue
}
} else break
# Out of arguments, yet still getting a non-option word.
if {![A size]} { my tooMany }
my ProcessArgument
if {![P size]} break
}
# At this point P is empty. A may not be. That is ok if the
# remaining A's are optional. Simply scan them, those which
# are mandatory will throw the necessary error.
debug.cmdr/config {remainder: [A size]}
while {[A size]} {
set argname [A get]
debug.cmdr/config {@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@}
debug.cmdr/config {$argname, a[A size] p[P size]}
[dict get $mymap $argname] process $argname $mypq
debug.cmdr/config {/////////////////////////////////}
}
debug.cmdr/validate {done remainder}
#puts "a[A size] p[P size]"
# End conditions:
# P left, A empty. - wrong#args, too many.
# A left, P empty. - wrong#args, not enough.
# A, P empty. - OK
# Note that 'not enough' should not be reached here, but in
# the parameter instances. I.e. early.
if {![A size] && [P size]} { my tooMany }
if {![P size] && [A size]} { my notEnough }
# XXX Go through the regular arguments and validate them?
# XXX Or can we assume that things will work simply through
# XXX access by the command ?
debug.cmdr/config {/done all}
return
}
# # ## ### ##### ######## #############
method ProcessArgument {} {
debug.cmdr/config {}
# Note: The parameter instance is responsible for retrieving
# its value from the parameter queue. It may pass on this.
# This also checks if there is enough in the P queue, aborting
# if not.
set argname [A get]
#puts [A size]|$argname|[P size]
[dict get $mymap $argname] process $argname $mypq
#puts \t==>[P size]
return
}
method ProcessOption {} {
debug.cmdr/config {}
# Get option. Do special handling.
# Non special option gets dispatched to handler (cmdr::parameter instance).
# The handler is responsible for retrieved the option's value.
set option [P get]
# Handle general special forms:
#
# --foo=bar ==> --foo bar
# -f=bar ==> -f bar
if {[regexp {^(-[^=]+)=(.*)$} $option --> option value]} {
P unget $value
}
# Validate existence of the option
if {![dict exists $myfullopt $option]} {
return -code error \
-errorcode {CMDR CONFIG BAD OPTION} \
"Unknown option $option"
}
# Map from option prefix to full option
set options [dict get $myfullopt $option]
if {[llength $options] > 1} {
return -code error \
-errorcode {CMDR CONFIG AMBIGUOUS OPTION} \
"Ambiguous option prefix $option, matching [join $options {, }]"
}
# Now map the fully expanded option name to its handler and
# let it deal with the remaining things, including retrieval
# of the option argument (if any), validation, etc.
[dict get $myoption [lindex $options 0]] process $option $mypq
return
}
method tooMany {} {
return -code error \
-errorcode {CMDR CONFIG WRONG-ARGS TOO-MANY} \
"wrong#args, too many"
}
method notEnough {} {
return -code error \
-errorcode {CMDR CONFIG WRONG-ARGS NOT-ENOUGH} \
"wrong#args, not enough"
}
method missingOptionValue {name} {
return -code error \
-errorcode {CMDR CONFIG WRONG-ARGS OPTION NOT-ENOUGH} \
"wrong#args, missing value for option '$name'"
}
# # ## ### ##### ######## #############
variable mymap mypub myoption myfullopt myargs mynames \
myaq mypq mycchain myreplexit myreplok myreplcommit \
myreset myred mygreen mycyan myinteractive myinforce \
mydisplay myreplskip
# # ## ### ##### ######## #############
## Local shell for interactive entry of the parameters in the collection.
method interact {} {
debug.cmdr/config {}
# compare cmdr::officer REPL (=> method "do").
set shell [linenoise::facade new [self]]
set myreplexit 0 ; # Flag: Stop repl, not yet.
set myreplok 0 ; # Flag: We can't commit properly
set myreplcommit 0 ; # Flag: We are not asked to commit yet.
set myreplskip 0 ; # Flag: Do not report.
my ShowState
$shell history 1
try {
$shell repl
} trap {CMDR CONFIG INTERACT CANCEL} {e o} {
return 0
} trap {CMDR CONFIG INTERACT OK} {e o} {
if {!$myreplok} {
# Bad commit with incomplete data.
return -code error \
-errorcode {CMDR CONFIG COMMIT FAIL} \
"Unable to perform \"[context fullname]\", incomplete or bad arguments"
}
return 1
} finally {
$shell destroy
}
# Regular return indicates abort (^D), treat as cancellation.
return 0
}
# # ## ### ##### ######## #############
## Shell hook methods called by the linenoise::facade.
method prompt1 {} { return "[context dname] > " }
method prompt2 {} { error {Continuation lines are not supported} }
method continued {line} { return 0 }
method exit {} { return $myreplexit }
method dispatch {cmd} {
debug.cmdr/config {}
if {$cmd eq {}} {
# No command, do nothing.
set myreplskip 1
return
}
switch -exact -- $cmd {
.run - .ok {
set myreplexit 1
set myreplcommit 1
return
}
.exit - .cancel {
set myreplexit 1
return
}
.help {
puts [cmdr help format full \
[linenoise columns] \
[dict create [context dname] \
[my help interact]]]
return
}
}
set words [lassign [string token shell $cmd] cmd]
# cmd = parameter name, words = parameter value.
# Note: Most pseudo commands take a single argument!
# Presence-only options are the exception.
# Note: The lookup accepts the undocumented parameters as
# well, despite them not shown by ShowState, nor
# available for completion.
set para [my lookup $cmd]
if {[$para presence] && ([llength $words] != 0)} {
return -code error -errorcode {CMDR CONFIG WRONG ARGS} \
"wrong \# args: should be \"$cmd\""
}
if {[llength $words] < 1} {
$para interact
return
}
if {![$para list] && [llength $words] > 1} {
return -code error -errorcode {CMDR CONFIG WRONG ARGS} \
"wrong \# args: should be \"$cmd value\""
}
# cmd is option => Add the nessary dashes? No. Only needed for
# boolean special form, and direct interaction does not allow
# that.
if {[$para presence]} {
# See also cmdr::parameter/ProcessOption
$para set yes
} elseif {[$para list]} {
foreach w $words { $para set $w }
} else {
$para set {*}$words
}
return
}
method report {what data} {
debug.cmdr/config {}
if {$myreplskip} {
set myreplskip 0
return
}
if {$myreplexit} {
if {$myreplcommit} {
return -code error -errorcode {CMDR CONFIG INTERACT OK} ""
} else {
return -code error -errorcode {CMDR CONFIG INTERACT CANCEL} ""
}
}
my ShowState
switch -exact -- $what {
ok {
if {$data eq {}} return
puts stdout $data
}
fail {
puts stderr $data
}
default {
return -code error \
"Internal error, bad result type \"$what\", expected ok, or fail"
}
}
}
# # ## ### ##### ######## #############
# Shell hook method - Command line completion.
method complete {line} {
debug.cmdr/config {} 10
#puts stderr ////////$line
try {
set completions [my complete-repl [context parse-line $line]]
} on error {e o} {
puts stderr "ERROR: $e"
puts stderr $::errorInfo
set completions {}
}
#puts stderr =($completions)
return $completions
}
method complete-repl {parse} {
debug.cmdr/config {} 10
#puts stderr [my fullname]/[self]/$parse/
dict with parse {}
# -> line, words, nwords, ok, at, doexit
if {!$ok} {
#puts stderr \tBAD
return {}
}
# All arguments and options are (pseudo-)commands.
# The special exit commands as well.
set commands [my Visible]
lappend commands .ok .run
lappend commands .cancel .exit
lappend commands .help
set commands [lsort -unique [lsort -dict $commands]]
if {$line eq {}} {
return $commands
}
if {$nwords == 1} {
# Match among the arguments, options, and specials
return [context completions $parse [context match $parse $commands]]
}
if {$nwords == 2} {
# Locate the responsible parameter and let it complete.
# Note: Here we non-public parameters as well.
set matches [context match $parse [my names]]
if {[llength $matches] == 1} {
# Proper subordinate found. Delegate. Note: Step to next
# word, we have processed the current one, the command.
dict incr parse at
set para [my lookup [lindex $matches 0]]
# Presence-only options do not have an argument to complete.
if {[$para presence]} {
return {}
}
return [context completions $parse [$para complete-words $parse]]
}
# No completion if nothing found, or ambiguous.
return {}
}
# No completion beyond the command and 1st argument.
return {}
}
method Visible {} {
set visible {}
foreach p [my names] {
if {![dict exists $mypub $p] &&
![[my lookup $p] defined?]
} continue
# Keep public elements, and any hidden ones already having
# a user definition. The user obviously knows about them.
lappend visible $p
}
return $visible
}
method dump {} {
my PrintState [my names] 1
}
method ShowState {} {
puts [my display]
flush stdout
return
}
method PrintState {plist {full 0}} {
set header [context dname]
set plist [lsort -dict $plist]
set labels [cmdr util padr $plist]
set blank [string repeat { } [string length [lindex $labels 0]]]
# Recalculate the value of changed parameters. (They have
# 'forgotten' their value due to 'set'). We disallow interaction
# for parameters who would normally do this to gather information
# from the user.
my Force 0 0
set text {}
set alldefined 1
set somebad 0
foreach label $labels para $plist {
set para [my lookup $para]
set label [string totitle $label 0 0]
set required [$para required]
set islist [$para list]
set defined [$para defined?]
try {
set value [$para value]
if {$value eq {}} {
set value ${mycyan}<<epsilon>>${myreset}
}
} trap {CMDR PARAMETER UNDEFINED} {e o} {
# Mandatory argument, without user-specified value.
set value "${mycyan}(undefined)$myreset"
} trap {CMDR VALIDATE} {e o} {
# Any argument with a bad value.
set value "[$para string] ${mycyan}($e)$myreset"
set somebad 1
}
append text { }
if {$required && !$defined} {
set label ${myred}$label${myreset}
set alldefined 0
} else {
#set label "$label "
}
if {$full} {
append label " ("
append label [expr {[$para ordered] ? "o":"-"}]
append label [expr {[$para cmdline] ? "c":"-"}]
append label [expr {[$para list] ? "L":"-"}]
append label [expr {[$para presence] ? "P":"-"}]
append label [expr {[$para documented] ? "d":"-"}]
append label [expr {[$para isbool] ? "B":"-"}]
append label [expr {[$para hasdefault] ? "D":"-"}]
append label [expr {[$para defined?] ? "!":"-"}]
append label [expr {[$para defered] ? ">":"-"}]
append label [expr {[$para required] ? "/.." : [$para threshold] < 0 ? "/pt":"/th"}]
append label ")"
set sfx { }
} else {
append label [expr {[$para list] ? " L":" "}]
set sfx { }
}
append text $label
append text { : }
if {!$islist} {
append text $value
} else {
set remainder [lassign $value first]
append text $first
foreach r $remainder {
append text "\n $blank$sfx : $r"
}
}
append text \n
}
if {$somebad} {
set text "$header (${myred}BAD$myreset):\n$text"
} elseif {!$alldefined} {
set text "$header (${myred}INCOMPLETE$myreset):\n$text"
} else {
set text "$header (${mygreen}OK$myreset):\n$text"
set myreplok 1
}
return $text
}
# # ## ### ##### ######## #############
method Colors {} {
if {$::tcl_platform(platform) eq "windows"} {
set myreset ""
set myred ""
set mygreen ""
set mycyan ""
} else {
set myreset [::term::ansi::code::ctrl::sda_reset]
set myred [::term::ansi::code::ctrl::sda_fgred]
set mygreen [::term::ansi::code::ctrl::sda_fggreen]
set mycyan [::term::ansi::code::ctrl::sda_fgcyan]
}
return
}
# # ## ### ##### ######## #############
}
# # ## ### ##### ######## ############# #####################
## Ready
package provide cmdr::config 0.3