2026-11-03 03:45:47 +00:00
package cobra
import (
"bytes"
"fmt"
"io"
"strings"
2026-11-03 04:06:05 +00:00
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
2026-11-03 03:45:47 +00:00
spfcobra "github.com/spf13/cobra"
)
2026-11-03 03:50:12 +00:00
// CompletionQueryToken is the hidden request understood by generated scripts.
const CompletionQueryToken = spfcobra . ShellCompRequestCmd
2026-11-03 04:06:05 +00:00
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" )
}
2026-11-03 13:12:05 +00:00
// The first line is always metadata, even for a whole-word replacement.
// Remaining lines before the directive are exclusively literal candidates.
if strings . ContainsAny ( result . Prefix , "\x00\r\n\t" ) {
return "" , fmt . Errorf ( "unrepresentable completion prefix" )
2026-11-03 04:06:05 +00:00
}
2026-11-03 13:12:05 +00:00
var output strings . Builder
output . WriteString ( completionPathPrefixMarker )
output . WriteString ( result . Prefix )
output . WriteByte ( '\n' )
2026-11-03 04:06:05 +00:00
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 )
}
2026-11-03 03:45:47 +00:00
// 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 ( )
2026-11-03 04:06:05 +00:00
if shell == "bash" {
script , err = adjustBashCompletion ( script , root . Name ( ) )
} else if shell == "zsh" {
script , err = adjustZshCompletion ( script )
2026-11-03 03:45:47 +00:00
} 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
}
2026-11-03 04:06:05 +00:00
func adjustBashCompletion ( script , name string ) ( string , error ) {
var err error
2026-11-03 13:09:00 +00:00
script , err = replaceCompletionSection ( script , " local requestComp lastParam lastChar args" , " # Extract the directive" , ` local - a requestWords = ( )
local word char quote decoded next i backtick = $ '\x60'
# Bash ' s completion helpers supply shell spelling , not evaluated argv .
# Remove quoting and escapes without expanding substitutions or globs .
for word in "${words[@]}" ; do
quote =
decoded =
for ( ( i = 0 ; i < $ { # word } ; i ++ ) ) ; do
char = $ { word : i : 1 }
case "$quote:$char" in
"':'" | ' ":" ' ) quote = ; ;
' : "'|" : ' " ) quote = $ char ; ;
: \ \ | '"' : \ \ )
if ( ( i + 1 < $ { # word } ) ) ; then
next = $ { word : i + 1 : 1 }
if [ [ $ quote == '"' && $ next != ' \ ' && $ next != '"' && $ next != '$' && $ next != "$backtick" ] ] ; then
decoded += $ char
continue
fi
( ( i += 1 ) )
decoded += $ { word : i : 1 }
else
decoded += $ char
fi ; ;
* ) decoded += $ char ; ;
esac
done
requestWords += ( "$decoded" )
done
if [ [ - z $ cur && $ { words [ $ { # words [ @ ] } - 1 ] } != *= ] ] ; then
# The Bash helpers may omit the empty word at a trailing space .
if ( ( $ { # requestWords [ @ ] } <= cword ) ) ; then requestWords += ( "" ) ; fi
fi
2026-11-03 13:11:07 +00:00
cur = $ { requestWords [ $ { # requestWords [ @ ] } - 1 ] }
2026-11-03 13:09:00 +00:00
out = $ ( COBRA_ACTIVE_HELP = 0 "${requestWords[0]}" __complete "${requestWords[@]:1}" 2 > / dev / null )
` )
if err != nil {
return "" , err
}
2026-11-03 04:06:05 +00:00
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
}
2026-11-03 13:12:05 +00:00
parsePrefix := ` if [[ $ { out} != ` + completionPathPrefixMarker + ` * ] ] ; then
compopt + o default
return
fi
completionPrefix = $ { out % % $ '\n' * }
completionPrefix = $ { completionPrefix # ` + completionPathPrefixMarker + ` }
completionOriginalCur = $ cur
if [ [ $ completionOriginalCur != "$completionPrefix" * ] ] ; then
compopt + o default
return
2026-11-03 04:06:05 +00:00
fi
2026-11-03 13:12:05 +00:00
if [ [ $ out == * $ '\n' * ] ] ; then out = $ { out # * $ '\n' } ; else out = ; fi
cur = $ { completionOriginalCur # "$completionPrefix" }
2026-11-03 04:06:05 +00:00
# Separate activeHelp from normal completions `
script , err = replaceCompletionText ( script , ` # Separate activeHelp from normal completions ` , parsePrefix )
2026-11-03 13:12:05 +00:00
if err != nil {
return "" , err
}
script , err = replaceCompletionSection ( script , " local activeHelpMarker=" , "\n}\n" , ` while IFS = ' ' read - r comp ; do
[ [ - z $ comp ] ] || completions += ( "$comp" )
done << < "${out}"
` )
2026-11-03 04:06:05 +00:00
if err != nil {
return "" , err
}
script , err = replaceCompletionText ( script ,
` if (((directive & shellCompDirectiveFilterFileExt) != 0)); then ` ,
2026-11-03 13:11:07 +00:00
` if ( ( directive == 0 ) ) ; then
2026-11-03 04:06:05 +00:00
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
2026-11-03 13:09:00 +00:00
script , err = replaceCompletionText ( script , ` words=("$ { =words[1,CURRENT]}") ` , ` words=("$ { (@Q)words[1,CURRENT]}") ` )
2026-11-03 04:06:05 +00:00
if err != nil {
return "" , err
}
2026-11-03 13:09:00 +00:00
script , err = replaceCompletionSection ( script , " # For zsh, when completing a flag" , " # Extract the directive" , ` local - a prefixArgs orderArgs spaceArgs
out = $ ( COBRA_ACTIVE_HELP = 0 "${words[1]}" __complete "${words[@]:1}" 2 > / dev / null )
` )
2026-11-03 04:06:05 +00:00
if err != nil {
return "" , err
}
2026-11-03 13:12:05 +00:00
script , err = replaceCompletionSection ( script , ` local activeHelpMarker= ` , " if [ $((directive & shellCompDirectiveNoSpace))" , ` [[ "$out" == ` + completionPathPrefixMarker + ` * ] ] || return 1
flagPrefix = "${out%%$'\n'*}"
flagPrefix = "${flagPrefix#`+completionPathPrefixMarker+`}"
[ [ "$lastParam" == "$flagPrefix" * ] ] || return 1
if [ [ $ out == * $ '\n' * ] ] ; then out = "${out#*$'\n'}" ; else out = ; fi
2026-11-03 13:09:00 +00:00
if [ [ - n "$flagPrefix" ] ] ; then
prefixArgs = ( - P "$flagPrefix" )
fi
2026-11-03 13:12:05 +00:00
local tab = "$(printf '\t')"
while IFS = read - r comp ; do
if [ [ - n "$comp" ] ] ; then
comp = $ { comp //:/\\:}
completions += ( "${comp//$tab/:}" )
fi
done << < "$out"
2026-11-03 04:06:05 +00:00
2026-11-03 13:12:05 +00:00
` )
2026-11-03 13:09:00 +00:00
if err != nil {
return "" , err
}
return replaceCompletionSection ( script , " if [ $((directive & shellCompDirectiveNoSpace))" , "\n}\n" , ` if [ $ ( ( directive & shellCompDirectiveNoSpace ) ) - ne 0 ] ; then
spaceArgs = ( - S "" )
fi
if [ $ ( ( directive & shellCompDirectiveKeepOrder ) ) - ne 0 ] ; then
orderArgs = ( - V )
fi
if [ $ ( ( directive & shellCompDirectiveNoFileComp ) ) - ne 0 ] ; then
_describe "${orderArgs[@]}" completions completions "${prefixArgs[@]}" "${spaceArgs[@]}"
else
if [ [ - n "$flagPrefix" ] ] ; then
compset - P "${(b)flagPrefix}"
fi
if [ $ ( ( directive & shellCompDirectiveFilterDirs ) ) - ne 0 ] ; then
_files - /
else
_files
fi
fi
` )
}
func replaceCompletionSection ( script , start , end , replacement string ) ( string , error ) {
i := strings . Index ( script , start )
if i < 0 {
return "" , fmt . Errorf ( "unsupported generated completion script" )
}
j := strings . Index ( script [ i + len ( start ) : ] , end )
if j < 0 {
return "" , fmt . Errorf ( "unsupported generated completion script" )
}
return script [ : i ] + replacement + script [ i + len ( start ) + j : ] , nil
2026-11-03 04:06:05 +00:00
}
2026-11-03 03:45:47 +00:00
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
2026-11-03 03:50:12 +00:00
cobraName := strings . NewReplacer ( "-" , "_" , ":" , "_" ) . Replace ( name )
variableName := strings . ReplaceAll ( cobraName , "." , "_" )
if cobraName != variableName {
script = strings . ReplaceAll ( script , "__" + cobraName + "_" , "__" + variableName + "_" )
}
2026-11-03 03:45:47 +00:00
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
}
2026-11-03 13:12:05 +00:00
script , err = replaceCompletionSection ( script , " # For Fish, when completing a flag" , ` printf "%s\n" "$directiveLine" ` , ` if not set -q comps[1]; or not string match -q -- ' ` + completionPathPrefixMarker + ` * ' "$comps[1]"
printf ":%s\n" ` +fmt.Sprint(spfcobra.ShellCompDirectiveError)+ `
return
2026-11-03 13:11:07 +00:00
end
2026-11-03 13:12:05 +00:00
set - l flagPrefix ( string replace -- ' ` +completionPathPrefixMarker+ ` ' ' ' "$comps[1]" )
set - q flagPrefix [ 1 ] ; or set flagPrefix ""
printf "%s\n" "$comps[1]"
set - e comps [ 1 ]
2026-11-03 13:11:07 +00:00
for comp in $ comps
printf "%s%s\n" "$flagPrefix" "$comp"
2026-11-03 04:06:05 +00:00
end
2026-11-03 13:11:07 +00:00
` )
2026-11-03 03:45:47 +00:00
if err != nil {
return "" , err
}
2026-11-03 03:50:12 +00:00
variable := "__" + variableName + "_comp_results"
2026-11-03 04:06:05 +00:00
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 )
2026-11-03 13:11:07 +00:00
if test ( count $ % [ 1 ] s ) - gt 0 ; and string match - q -- ' % [ 2 ] s * ' "$%[1]s[1]"
2026-11-03 04:06:05 +00:00
set - l pathPrefix ( string replace -- ' % [ 2 ] s ' ' ' "$%[1]s[1]" )
set - l token ( string unescape -- ( commandline - ct ) )
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 ` , variable , completionPathPrefixMarker , variableName )
script , err = replaceCompletionText ( script , ` set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) ` , pathContext )
if err != nil {
return "" , err
}
2026-11-03 13:11:07 +00:00
return script , nil
2026-11-03 03:45:47 +00:00
}