Files
dingtalk-workspace-cli/pkg/cmdutil/flagfix.go
T

244 lines
6.4 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// FlagFixResult holds the result of SuggestFlagFix analysis.
type FlagFixResult struct {
Suggestion string
AutoFixFlag string
AutoFixValue string
}
// CommonFlagAliases maps commonly misused flag names to their correct equivalents.
var CommonFlagAliases = map[string]string{
"json": "format json",
"output": "format",
"out": "format",
"o": "format",
"silent": "quiet",
"dry": "dry-run",
"force": "yes",
"f": "yes",
"timeout-seconds": "timeout",
"device-flow": "device",
"deviceflow": "device",
}
// DetectNumericTypeError checks if err is a Cobra/pflag numeric type
// validation error. Returns the flag name and the bad value if detected.
func DetectNumericTypeError(err error) (flagName, badValue string, ok bool) {
msg := err.Error()
if !strings.Contains(msg, "strconv.Parse") {
return "", "", false
}
const argPrefix = "invalid argument \""
argIdx := strings.Index(msg, argPrefix)
if argIdx < 0 {
return "", "", false
}
afterArg := msg[argIdx+len(argPrefix):]
argEnd := strings.Index(afterArg, "\"")
if argEnd < 0 {
return "", "", false
}
badVal := afterArg[:argEnd]
const marker = "\" for \"--"
idx := strings.Index(msg, marker)
if idx < 0 {
return "", "", false
}
rest := msg[idx+len(marker):]
endIdx := strings.Index(rest, "\" flag")
if endIdx < 0 {
return "", "", false
}
return rest[:endIdx], badVal, true
}
// flagFixCandidate reports whether f should participate in unknown-flag
// suggestion candidates and Flags: listings. Hidden flags (e.g. wukong's
// MarkHidden compatibility aliases) and internal json/params merge flags
// are skipped so the hint candidate set stays a subset of what --help shows.
func flagFixCandidate(f *pflag.Flag) bool {
if f == nil || f.Hidden {
return false
}
switch f.Name {
case "json", "params":
return false
}
return true
}
// VisibleFlagNames returns sorted candidate flag names for cmd.Flags()
// using flagFixCandidate. Intended for agent-facing error recovery
// (available_flags).
func VisibleFlagNames(cmd *cobra.Command) []string {
if cmd == nil {
return nil
}
seen := make(map[string]bool)
var names []string
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !flagFixCandidate(f) || seen[f.Name] {
return
}
seen[f.Name] = true
names = append(names, f.Name)
})
sort.Strings(names)
return names
}
// SuggestFlagFix detects flag-value concatenation errors, common flag aliases,
// and Levenshtein-close typos.
func SuggestFlagFix(cmd *cobra.Command, flagErr error) FlagFixResult {
msg := flagErr.Error()
const prefix = "unknown flag: --"
idx := strings.Index(msg, prefix)
if idx < 0 {
return FlagFixResult{}
}
body := strings.TrimSpace(msg[idx+len(prefix):])
if alias, ok := CommonFlagAliases[body]; ok {
return FlagFixResult{Suggestion: fmt.Sprintf("Did you mean --%s? Run '%s --help' for options", alias, cmd.CommandPath())}
}
var bestFlag, bestValue string
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !flagFixCandidate(f) {
return
}
name := f.Name
if strings.HasPrefix(body, name) && len(body) > len(name) {
if len(name) > len(bestFlag) {
bestFlag = name
bestValue = body[len(name):]
}
}
})
if bestFlag != "" {
lf := cmd.Flags().Lookup(bestFlag)
if lf != nil {
fmtStr := ""
if v := lf.Annotations["x-cli-format"]; len(v) > 0 {
fmtStr = v[0]
}
var enumCopy []string
if v := lf.Annotations["x-cli-enum"]; len(v) > 0 {
enumCopy = append([]string{}, v...)
}
flagType := lf.Value.Type()
if SuffixLooksLikeValue(bestValue, flagType, fmtStr, enumCopy) {
if flagType == "bool" || flagType == "boolean" {
normalized, _ := NormalizeBoolLiteral(bestValue)
return FlagFixResult{Suggestion: fmt.Sprintf("Use an explicit boolean value: --%s=%s", bestFlag, normalized)}
}
suggestion := fmt.Sprintf("Space required between flag and value: --%s %s", bestFlag, bestValue)
return FlagFixResult{Suggestion: suggestion, AutoFixFlag: bestFlag, AutoFixValue: bestValue}
}
}
}
bestName, bestDist := "", 999
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if !flagFixCandidate(f) {
return
}
d := LevenshteinDist(body, f.Name)
if d < bestDist {
bestDist = d
bestName = f.Name
}
})
threshold := LevenshteinThreshold(len(body))
if bestDist > 0 && bestDist <= threshold && bestName != "" {
suf := formatFlagHintSuffix(cmd.Flags().Lookup(bestName))
return FlagFixResult{Suggestion: fmt.Sprintf("Did you mean --%s?%s", bestName, suf)}
}
return FlagFixResult{Suggestion: fmt.Sprintf("Run '%s --help' to see available options", cmd.CommandPath())}
}
func formatFlagHintSuffix(f *pflag.Flag) string {
if f == nil {
return ""
}
var parts []string
if u := strings.TrimSpace(f.Usage); u != "" {
if len(u) > 100 {
u = u[:97] + "..."
}
parts = append(parts, u)
}
if v := f.Annotations["x-cli-format"]; len(v) > 0 && v[0] != "" {
parts = append(parts, "format="+v[0])
}
if len(parts) == 0 {
return ""
}
return " (" + strings.Join(parts, ", ") + ")"
}
// LevenshteinThreshold returns the max edit distance allowed based on string length.
func LevenshteinThreshold(nameLen int) int {
if nameLen <= 3 {
return 1
}
if nameLen <= 8 {
return 2
}
return 3
}
// LevenshteinDist returns the edit distance between two strings.
func LevenshteinDist(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
dp := make([]int, lb+1)
for j := 0; j <= lb; j++ {
dp[j] = j
}
for i := 1; i <= la; i++ {
prev := dp[0]
dp[0] = i
for j := 1; j <= lb; j++ {
temp := dp[j]
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
dp[j] = min(dp[j]+1, min(dp[j-1]+1, prev+cost))
prev = temp
}
}
return dp[lb]
}