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) for _, spelling := range []capturedFlagSpelling{{name: "help"}, {name: "h", short: true}} { 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()) }