283 lines
9.1 KiB
Go
283 lines
9.1 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
|
)
|
|
|
|
type resolvedFlagAlias struct {
|
|
source capturedFlagSpelling
|
|
target capturedFlagSpelling
|
|
}
|
|
|
|
type resolvedFlagAliases struct {
|
|
bySpelling map[string]resolvedFlagAlias
|
|
}
|
|
|
|
type flagAliasMapping struct {
|
|
target capturedFlagSpelling
|
|
scoped bool
|
|
}
|
|
|
|
type flagAliasContext struct {
|
|
path commandpath.Path
|
|
canonical map[string]capturedFlagSpelling
|
|
discovery bool
|
|
}
|
|
|
|
func (aliases resolvedFlagAliases) neutralRecords() []commandmeta.FlagAlias {
|
|
keys := make([]string, 0, len(aliases.bySpelling))
|
|
for key := range aliases.bySpelling {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
records := make([]commandmeta.FlagAlias, 0, len(keys))
|
|
for _, key := range keys {
|
|
alias := aliases.bySpelling[key]
|
|
records = append(records, commandmeta.FlagAlias{Source: alias.source.token(), Target: alias.target.token()})
|
|
}
|
|
return records
|
|
}
|
|
|
|
func (spelling capturedFlagSpelling) token() string {
|
|
if spelling.short {
|
|
return "-" + spelling.name
|
|
}
|
|
return "--" + spelling.name
|
|
}
|
|
|
|
func compileFlagAliases(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface {
|
|
Resolve(commandpath.Path) commandpath.Path
|
|
}) (map[string]resolvedFlagAliases, map[string]resolvedFlagAliases, error) {
|
|
canonicalNodes := indexCanonicalNodes(roots)
|
|
global := make(map[string]flagAliasMapping)
|
|
scoped := make(map[string]map[string]flagAliasMapping)
|
|
for _, rule := range rules {
|
|
record := rule.record
|
|
switch record.kind {
|
|
case globalFlagAliasRule:
|
|
source := flagSpellingKey(record.sourceFlag)
|
|
global[source] = flagAliasMapping{target: record.targetFlag}
|
|
case scopedFlagAliasRule:
|
|
scope := record.scopePath.Key()
|
|
if canonicalNodes[scope] == nil {
|
|
return nil, nil, fmt.Errorf("flag alias scope %q is not a canonical command path", record.scopePath.String())
|
|
}
|
|
if scoped[scope] == nil {
|
|
scoped[scope] = make(map[string]flagAliasMapping)
|
|
}
|
|
source := flagSpellingKey(record.sourceFlag)
|
|
scoped[scope][source] = flagAliasMapping{target: record.targetFlag, scoped: true}
|
|
}
|
|
}
|
|
|
|
canonicalContexts := make(map[string]flagAliasContext, len(canonicalNodes)+1)
|
|
rootPath := commandpath.Path(nil)
|
|
canonicalContexts[rootPath.Key()] = flagAliasContext{path: rootPath, canonical: helpFlagSpellings()}
|
|
for key, node := range canonicalNodes {
|
|
canonicalContexts[key] = flagAliasContext{path: node.path.Clone(), canonical: canonicalFlagSpellings(node.command)}
|
|
}
|
|
discoveryContexts := discoverFlagContexts(rules, roots, aliases, canonicalNodes)
|
|
|
|
canonicalKeys := sortedContextKeys(canonicalContexts)
|
|
resolvedCanonical := make(map[string]resolvedFlagAliases, len(canonicalContexts))
|
|
for _, key := range canonicalKeys {
|
|
context := canonicalContexts[key]
|
|
mappings := cloneFlagMappings(global)
|
|
for source, mapping := range scoped[key] {
|
|
mappings[source] = mapping
|
|
}
|
|
resolved, err := resolveFlagAliases(context, mappings)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
resolvedCanonical[key] = resolved
|
|
}
|
|
|
|
discoveryKeys := sortedContextKeys(discoveryContexts)
|
|
resolvedDiscovery := make(map[string]resolvedFlagAliases, len(discoveryContexts))
|
|
for _, key := range discoveryKeys {
|
|
context := discoveryContexts[key]
|
|
resolved, err := resolveFlagAliases(context, cloneFlagMappings(global))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
resolvedDiscovery[key] = resolved
|
|
}
|
|
return resolvedCanonical, resolvedDiscovery, nil
|
|
}
|
|
|
|
func indexCanonicalNodes(roots map[string]*canonicalNode) map[string]*canonicalNode {
|
|
indexed := make(map[string]*canonicalNode)
|
|
var add func(*canonicalNode)
|
|
add = func(node *canonicalNode) {
|
|
indexed[node.path.Key()] = node
|
|
for _, child := range node.children {
|
|
add(child)
|
|
}
|
|
}
|
|
for _, root := range roots {
|
|
add(root)
|
|
}
|
|
return indexed
|
|
}
|
|
|
|
func helpFlagSpellings() map[string]capturedFlagSpelling {
|
|
flags := make(map[string]capturedFlagSpelling, 2+len(commandmeta.ReportingFlags()))
|
|
for _, spelling := range []capturedFlagSpelling{{name: "help"}, {name: "h", short: true}} {
|
|
flags[flagSpellingKey(spelling)] = spelling
|
|
}
|
|
for _, metadata := range commandmeta.ReportingFlags() {
|
|
spelling := capturedFlagSpelling{name: metadata.Name}
|
|
flags[flagSpellingKey(spelling)] = spelling
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func canonicalFlagSpellings(command Command) map[string]capturedFlagSpelling {
|
|
flags := helpFlagSpellings()
|
|
if command.record.payload == nil {
|
|
return flags
|
|
}
|
|
for _, metadata := range command.record.payload.bridgeFlags() {
|
|
long := capturedFlagSpelling{name: metadata.Name}
|
|
flags[flagSpellingKey(long)] = long
|
|
if metadata.Short != "" {
|
|
short := capturedFlagSpelling{name: metadata.Short, short: true}
|
|
flags[flagSpellingKey(short)] = short
|
|
}
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func discoverFlagContexts(rules []RoutingRule, roots map[string]*canonicalNode, aliases interface {
|
|
Resolve(commandpath.Path) commandpath.Path
|
|
}, canonicalNodes map[string]*canonicalNode) map[string]flagAliasContext {
|
|
commandSources := make(map[string]struct{})
|
|
prefixes := make(map[string]commandpath.Path)
|
|
for _, rule := range rules {
|
|
if rule.record.kind == commandAliasRule {
|
|
commandSources[rule.record.sourcePath.Key()] = struct{}{}
|
|
}
|
|
}
|
|
for _, rule := range rules {
|
|
if rule.record.kind != commandAliasRule {
|
|
continue
|
|
}
|
|
for length := 1; length < len(rule.record.sourcePath); length++ {
|
|
prefix := rule.record.sourcePath[:length].Clone()
|
|
key := prefix.Key()
|
|
if canonicalNodes[key] != nil {
|
|
continue
|
|
}
|
|
if _, exactAlias := commandSources[key]; exactAlias {
|
|
continue
|
|
}
|
|
resolved := aliases.Resolve(prefix)
|
|
trace := traceCanonicalPath(roots, resolved)
|
|
if trace.exact != nil || (trace.operandOwner != nil && acceptsOperandPrefix(trace.operandOwner.command, len(resolved)-len(trace.operandOwner.path))) {
|
|
continue
|
|
}
|
|
prefixes[key] = prefix
|
|
}
|
|
}
|
|
contexts := make(map[string]flagAliasContext, len(prefixes))
|
|
for key, path := range prefixes {
|
|
contexts[key] = flagAliasContext{path: path, canonical: helpFlagSpellings(), discovery: true}
|
|
}
|
|
return contexts
|
|
}
|
|
|
|
func cloneFlagMappings(mappings map[string]flagAliasMapping) map[string]flagAliasMapping {
|
|
cloned := make(map[string]flagAliasMapping, len(mappings))
|
|
for source, mapping := range mappings {
|
|
cloned[source] = mapping
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func sortedContextKeys(contexts map[string]flagAliasContext) []string {
|
|
keys := make([]string, 0, len(contexts))
|
|
for key := range contexts {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func resolveFlagAliases(context flagAliasContext, mappings map[string]flagAliasMapping) (resolvedFlagAliases, error) {
|
|
result := resolvedFlagAliases{bySpelling: make(map[string]resolvedFlagAlias)}
|
|
sources := make([]string, 0, len(mappings))
|
|
for source := range mappings {
|
|
sources = append(sources, source)
|
|
}
|
|
sort.Strings(sources)
|
|
for _, sourceKey := range sources {
|
|
mapping := mappings[sourceKey]
|
|
source := spellingFromKey(sourceKey)
|
|
target, active, err := resolveFlagTarget(source, mapping.target, mappings, context.canonical)
|
|
if err != nil {
|
|
return resolvedFlagAliases{}, fmt.Errorf("%s: %w", flagContextDescription(context), err)
|
|
}
|
|
if !active {
|
|
if mapping.scoped {
|
|
return resolvedFlagAliases{}, fmt.Errorf("scoped flag alias %s has no canonical destination in %s", source.token(), flagContextDescription(context))
|
|
}
|
|
continue
|
|
}
|
|
if _, collision := context.canonical[sourceKey]; collision {
|
|
return resolvedFlagAliases{}, fmt.Errorf("flag alias %s conflicts with a canonical spelling in %s", source.token(), flagContextDescription(context))
|
|
}
|
|
result.bySpelling[sourceKey] = resolvedFlagAlias{source: source, target: target}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func resolveFlagTarget(source, target capturedFlagSpelling, mappings map[string]flagAliasMapping, canonical map[string]capturedFlagSpelling) (capturedFlagSpelling, bool, error) {
|
|
visited := map[string]int{flagSpellingKey(source): 0}
|
|
chain := []capturedFlagSpelling{source}
|
|
current := target
|
|
for {
|
|
key := flagSpellingKey(current)
|
|
if endpoint, exists := canonical[key]; exists {
|
|
return endpoint, true, nil
|
|
}
|
|
if start, exists := visited[key]; exists {
|
|
cycle := append(append([]capturedFlagSpelling(nil), chain[start:]...), current)
|
|
spellings := make([]string, 0, len(cycle))
|
|
for _, spelling := range cycle {
|
|
spellings = append(spellings, spelling.token())
|
|
}
|
|
return capturedFlagSpelling{}, false, fmt.Errorf("flag alias cycle: %s", strings.Join(spellings, " → "))
|
|
}
|
|
visited[key] = len(chain)
|
|
chain = append(chain, current)
|
|
next, exists := mappings[key]
|
|
if !exists {
|
|
return capturedFlagSpelling{}, false, nil
|
|
}
|
|
current = next.target
|
|
}
|
|
}
|
|
|
|
func spellingFromKey(key string) capturedFlagSpelling {
|
|
if strings.HasPrefix(key, "short:") {
|
|
return capturedFlagSpelling{name: strings.TrimPrefix(key, "short:"), short: true}
|
|
}
|
|
return capturedFlagSpelling{name: strings.TrimPrefix(key, "long:")}
|
|
}
|
|
|
|
func flagContextDescription(context flagAliasContext) string {
|
|
if context.discovery {
|
|
return fmt.Sprintf("alias prefix %q", context.path.String())
|
|
}
|
|
if len(context.path) == 0 {
|
|
return "application root"
|
|
}
|
|
return fmt.Sprintf("command %q", context.path.String())
|
|
}
|