package commandkit import ( "fmt" "io" "strings" "unicode" internalcobra "go.maximumdirect.net/commandkit/internal/cobra" commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta" ) // Completion declares opt-in shell script generation for the executable // basename used to invoke the application. Install it at the application root. // Generated shell identifiers support letters, digits, underscores, dots, // hyphens, and colons; a leading hyphen is not supported. func Completion(executable string) Command { children := make([]Command, 0, 3) for _, shell := range []string{"bash", "zsh", "fish"} { shell := shell children = append(children, NewCommand(CommandSpec[struct{}]{ Name: shell, Summary: "Generate " + shell + " completion script.", Run: func(invocation Invocation, _ struct{}) Outcome { _ = invocation.Reporter().RenderOutput(func(writer io.Writer) error { return internalcobra.CompletionScript(writer, executable, shell) }) return Success() }, })) } command := RootGroup("completion", "Generate shell completion scripts.", children...) command.record.completion = true if !validCompletionExecutable(executable) { return WithError(command, fmt.Errorf("unsupported completion executable basename %q", executable)) } return command } // Cobra emits the basename into shell identifiers and unquoted command words. func validCompletionExecutable(name string) bool { if name == "" || name == "." || name == ".." || strings.HasPrefix(name, "-") { return false } for _, ch := range name { if !(unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '-' || ch == ':' || ch == '.') { return false } } return true } // CompletionChoice is a literal suggested value and its optional description. type CompletionChoice struct { // Value is inserted as data; it is not interpreted as shell source. Value string // Description is optional context displayed by shells that support it. Description string } // CompletionHint selects suggestions for a flag or positional value. Its zero // value requests no value suggestions; hints do not constrain accepted input. type CompletionHint struct { kind completionHintKind choices *completionChoicesData } // Keep the public metadata structs comparable without exposing mutable choices. type completionChoicesData struct { values []CompletionChoice } type completionHintKind uint8 const ( completionNone completionHintKind = iota completionChoices completionFiles completionDirectories ) // CompleteChoices suggests literal values matching the current value prefix. // The supplied slice is copied. An empty list produces no suggestions. Empty // values and values containing a newline, tab, or NUL cannot be presented as // suggestions, but remain valid input if the command's converter accepts them. func CompleteChoices(choices ...CompletionChoice) CompletionHint { hint := CompletionHint{kind: completionChoices} if len(choices) != 0 { hint.choices = &completionChoicesData{values: append([]CompletionChoice(nil), choices...)} } return hint } // CompleteFiles requests shell filename completion. func CompleteFiles() CompletionHint { return CompletionHint{kind: completionFiles} } // CompleteDirectories requests shell directory completion. func CompleteDirectories() CompletionHint { return CompletionHint{kind: completionDirectories} } // NoCompletion requests no value suggestions. func NoCompletion() CompletionHint { return CompletionHint{} } func bridgeCompletion(hint CompletionHint) commandmeta.CompletionHint { result := commandmeta.CompletionHint{} switch hint.kind { case completionChoices: result.Kind = commandmeta.CompletionChoices if hint.choices == nil { break } for _, choice := range hint.choices.values { result.Choices = append(result.Choices, commandmeta.CompletionChoice{Value: choice.Value, Description: choice.Description}) } case completionFiles: result.Kind = commandmeta.CompletionFiles case completionDirectories: result.Kind = commandmeta.CompletionDirectories } return result }