package cobra import ( "bytes" "fmt" "io" "strings" commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta" spfcobra "github.com/spf13/cobra" ) // CompletionQueryToken is the hidden request understood by generated scripts. const CompletionQueryToken = spfcobra.ShellCompRequestCmd const completionPathPrefixMarker = "__commandkit_path_prefix__" // CompletionReply translates neutral suggestions to the shell line protocol. func CompletionReply(result commandmeta.CompletionResult) (string, error) { var directive spfcobra.ShellCompDirective switch result.Kind { case commandmeta.CompletionNone, commandmeta.CompletionChoices: directive = spfcobra.ShellCompDirectiveNoFileComp case commandmeta.CompletionFiles: directive = spfcobra.ShellCompDirectiveDefault case commandmeta.CompletionDirectories: directive = spfcobra.ShellCompDirectiveFilterDirs default: return "", fmt.Errorf("unsupported completion suggestion strategy") } var output strings.Builder if result.Kind != commandmeta.CompletionNone && result.Prefix != "" && !strings.Contains(result.Prefix, "=") { output.WriteString(completionPathPrefixMarker) output.WriteString(result.Prefix) output.WriteByte('\n') } if result.Kind == commandmeta.CompletionChoices { for _, choice := range result.Choices { if choice.Value == "" || strings.ContainsAny(choice.Value, "\x00\r\n\t") { continue } // Generated scripts restore the attached prefix when needed. output.WriteString(choice.Value) if choice.Description != "" { output.WriteByte('\t') output.WriteString(strings.NewReplacer("\x00", " ", "\r", " ", "\n", " ", "\t", " ").Replace(choice.Description)) } output.WriteByte('\n') } } fmt.Fprintf(&output, ":%d\n", directive) return output.String(), nil } // CompletionErrorReply is the protocol response for a malformed query. func CompletionErrorReply() string { return fmt.Sprintf(":%d\n", spfcobra.ShellCompDirectiveError) } // CompletionScript generates a shell script for the installed executable. func CompletionScript(w io.Writer, executable, shell string) error { root := &spfcobra.Command{Use: executable} var generated bytes.Buffer var err error switch shell { case "bash": err = root.GenBashCompletionV2(&generated, true) case "zsh": err = root.GenZshCompletion(&generated) case "fish": err = root.GenFishCompletion(&generated, true) default: return fmt.Errorf("unsupported completion shell %q", shell) } if err != nil { return err } script := generated.String() if shell == "bash" { script, err = adjustBashCompletion(script, root.Name()) } else if shell == "zsh" { script, err = adjustZshCompletion(script) } else if shell == "fish" { script, err = adjustFishCompletion(script, root.Name()) } if err != nil { return err } n, err := io.WriteString(w, script) if err == nil && n != len(script) { return io.ErrShortWrite } return err } func adjustBashCompletion(script, name string) (string, error) { var err error script, err = replaceCompletionText(script, `local out directive`, `local out directive completionPrefix completionOriginalCur`) if err != nil { return "", err } errorDebug := fmt.Sprintf(`__%s_debug "Received error from custom completion go code"`, name) script, err = replaceCompletionText(script, errorDebug, errorDebug+"\n compopt +o default") if err != nil { return "", err } parsePrefix := `if [[ ${out} == ` + completionPathPrefixMarker + `-* ]]; then completionPrefix=${out%%$'\n'*} completionPrefix=${completionPrefix#` + completionPathPrefixMarker + `} completionOriginalCur=${words[${#words[@]}-1]} if [[ $completionPrefix != --* && $completionOriginalCur == "$completionPrefix"* ]]; then out=${out#*$'\n'} cur=${completionOriginalCur#"$completionPrefix"} else completionPrefix="" fi fi # Separate activeHelp from normal completions` script, err = replaceCompletionText(script, `# Separate activeHelp from normal completions`, parsePrefix) if err != nil { return "", err } script, err = replaceCompletionText(script, `if (((directive & shellCompDirectiveFilterFileExt) != 0)); then`, `if [[ -n $completionPrefix ]] && (((directive & shellCompDirectiveFilterDirs) == 0)) && (((directive & shellCompDirectiveNoFileComp) == 0)); then compopt +o default _filedir elif (((directive & shellCompDirectiveFilterFileExt) != 0)); then`) if err != nil { return "", err } old := fmt.Sprintf("__%[1]s_handle_special_char \"$cur\" :\n __%[1]s_handle_special_char \"$cur\" =", name) replacement := fmt.Sprintf(`if [[ -n $completionPrefix ]]; then compopt +o default local i for i in "${!COMPREPLY[@]}"; do COMPREPLY[$i]="${completionPrefix}${COMPREPLY[$i]}"; done cur=$completionOriginalCur fi __%[1]s_handle_special_char "$cur" : __%[1]s_handle_special_char "$cur" =`, name) return replaceCompletionText(script, old, replacement) } func adjustZshCompletion(script string) (string, error) { var err error script, err = replaceCompletionText(script, `words=("${=words[1,CURRENT]}")`, `words=("${(@)words[1,CURRENT]}")`) if err != nil { return "", err } script, err = replaceCompletionText(script, `=~ '-.*='`, `=~ '-[^=]*='`) if err != nil { return "", err } return replaceCompletionText(script, `local activeHelpMarker="_activeHelp_ "`, `if [[ "$out" == `+completionPathPrefixMarker+`-* ]]; then local pathPrefix="${out%%$'\n'*}" pathPrefix="${pathPrefix#`+completionPathPrefixMarker+`}" if [[ "$pathPrefix" != --* && "$lastParam" == "$pathPrefix"* ]]; then out="${out#*$'\n'}" if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then flagPrefix="-P ${pathPrefix}" else flagPrefix="" compset -P "${(b)pathPrefix}" fi fi fi local activeHelpMarker="_activeHelp_ "`) } func replaceCompletionText(script, old, replacement string) (string, error) { if strings.Count(script, old) != 1 { return "", fmt.Errorf("unsupported generated completion script") } return strings.Replace(script, old, replacement, 1), nil } func adjustFishCompletion(script, name string) (string, error) { var err error cobraName := strings.NewReplacer("-", "_", ":", "_").Replace(name) variableName := strings.ReplaceAll(cobraName, ".", "_") if cobraName != variableName { script = strings.ReplaceAll(script, "__"+cobraName+"_", "__"+variableName+"_") } script, err = replaceCompletionText(script, `set -l lastArg (string escape -- (commandline -ct))`, "set -l lastArg (string unescape -- (commandline -ct))\n set -q lastArg[1]; or set lastArg \"\"") if err != nil { return "", err } script, err = replaceCompletionText(script, `set -l results (eval $requestComp 2> /dev/null)`, `set -lx COBRA_ACTIVE_HELP 0`+"\n "+`set -l results ($args[1] `+spfcobra.ShellCompRequestCmd+` $args[2..-1] "$lastArg" 2> /dev/null)`) if err != nil { return "", err } script, err = replaceCompletionText(script, `string match -r -- '-.*=' "$lastArg"`, `string match -r -- '-[^=]*=' "$lastArg"`) if err != nil { return "", err } script, err = replaceCompletionText(script, `for comp in $comps`, `if set -q comps[1]; and string match -q -- '`+completionPathPrefixMarker+`-*' "$comps[1]" set -l candidatePrefix (string replace -- '`+completionPathPrefixMarker+`' '' "$comps[1]") if not string match -q -- '--*' "$candidatePrefix"; and test (string sub -l (string length -- "$candidatePrefix") -- "$lastArg") = "$candidatePrefix" set flagPrefix "$candidatePrefix" set -e comps[1] printf "%s%s\n" '`+completionPathPrefixMarker+`' "$flagPrefix" end end for comp in $comps`) if err != nil { return "", err } variable := "__" + variableName + "_comp_results" errorBranch := fmt.Sprintf(`__%[1]s_debug "Received error directive: aborting." # Might as well do file completion, in case it helps return 1`, variableName) errorReplacement := fmt.Sprintf(`__%[1]s_debug "Received error directive: aborting." return 0`, variableName) script, err = replaceCompletionText(script, errorBranch, errorReplacement) if err != nil { return "", err } pathContext := fmt.Sprintf(`set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) if test (count $%[1]s) -gt 0; and string match -q -- '%[2]s-*' "$%[1]s[1]" set -l pathPrefix (string replace -- '%[2]s' '' "$%[1]s[1]") set -l token (string unescape -- (commandline -ct)) if not string match -q -- '--*' "$pathPrefix"; and test (string sub -l (string length -- "$pathPrefix") -- "$token") = "$pathPrefix" set -e %[1]s[1] set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) if test $nofiles -eq 1 return 0 end set -l value (string sub -s (math (string length -- "$pathPrefix") + 1) -- "$token") set -q value[1]; or set value "" set -l escaped (string escape --no-quoted -- "$value" | string replace -a -- '=' '\\=') set --global %[1]s if test $dirfilter -eq 1 for entry in (__fish_complete_directories "$escaped") set -a %[1]s "$pathPrefix$entry" end else for entry in (complete -C "'' $escaped") set -a %[1]s "$pathPrefix$entry" end end __%[3]s_debug "Path completions: $%[1]s" return 0 end end`, variable, completionPathPrefixMarker, variableName) script, err = replaceCompletionText(script, `set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2)`, pathContext) if err != nil { return "", err } directories := fmt.Sprintf(`if test $dirfilter -eq 1 set -l flagPrefix (string match -r -- '^-[^=]*=' (commandline -ct)) set --global %[1]s for dir in (__fish_complete_directories) set -a %[1]s "$flagPrefix$dir" end return 0 end if test $filefilter -eq 1`, variable) return replaceCompletionText(script, `if test $filefilter -eq 1; or test $dirfilter -eq 1`, directories) }