Skip to content

Commit

Permalink
Merge pull request #1 from rsteube/bash
Browse files Browse the repository at this point in the history
bash
  • Loading branch information
rsteube authored Mar 24, 2020
2 parents 383ddb5 + a280374 commit daba7c3
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 2 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

[![CircleCI](https://circleci.com/gh/rsteube/carapace.svg?style=svg)](https://circleci.com/gh/rsteube/carapace)

[ZSH completion](https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org) and [Fish completion](https://fishshell.com/docs/current/#writing-your-own-completions) script generator for [cobra] (based on [spf13/cobra#646](https://github.com/spf13/cobra/pull/646)).
Completion script generator for [cobra] with support for:

- Bash
- [Fish](https://fishshell.com/docs/current/#writing-your-own-completions)
- [ZSH](https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org)


## Status
Expand Down
16 changes: 16 additions & 0 deletions action.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package carapace

import (
"github.com/rsteube/carapace/bash"
"github.com/rsteube/carapace/fish"
"github.com/rsteube/carapace/zsh"
)

type Action struct {
Bash string
Fish string
Zsh string
Callback CompletionCallback
Expand All @@ -17,6 +19,7 @@ type CompletionCallback func(args []string) Action
func (a Action) finalize(uid string) Action {
if a.Callback != nil {
// TODO only set to callback if no value is set (one shell might not need the callback)
a.Bash = bash.Callback(uid)
a.Fish = fish.Callback(uid)
a.Zsh = zsh.Callback(uid)
}
Expand All @@ -31,6 +34,7 @@ func ActionCallback(callback CompletionCallback) Action {
// ActionExecute uses command substitution to invoke a command and evalues it's result as Action
func ActionExecute(command string) Action {
return Action{
Bash: bash.ActionExecute(command),
Fish: fish.ActionExecute(command),
Zsh: zsh.ActionExecute(command),
}
Expand All @@ -39,6 +43,7 @@ func ActionExecute(command string) Action {
// ActionBool completes true/false
func ActionBool() Action {
return Action{
Bash: bash.ActionBool(),
Fish: fish.ActionBool(),
Zsh: zsh.ActionBool(),
}
Expand All @@ -47,13 +52,15 @@ func ActionBool() Action {
// ActionPathFiles completes filepaths
func ActionPathFiles(suffix string) Action {
return Action{
Bash: bash.ActionPathFiles(suffix),
Fish: fish.ActionPathFiles(suffix),
Zsh: zsh.ActionPathFiles("*" + suffix),
}
}

func ActionFiles(suffix string) Action {
return Action{
Bash: bash.ActionFiles(suffix),
Fish: fish.ActionFiles(suffix),
Zsh: zsh.ActionFiles("*" + suffix),
}
Expand All @@ -62,6 +69,7 @@ func ActionFiles(suffix string) Action {
// ActionNetInterfaces completes network interface names
func ActionNetInterfaces() Action {
return Action{
Bash: bash.ActionNetInterfaces(),
Fish: fish.ActionNetInterfaces(),
Zsh: zsh.ActionNetInterfaces(),
}
Expand All @@ -70,6 +78,7 @@ func ActionNetInterfaces() Action {
// ActionUsers completes user names
func ActionUsers() Action {
return Action{
Bash: bash.ActionUsers(),
Fish: fish.ActionUsers(),
Zsh: zsh.ActionUsers(),
}
Expand All @@ -78,6 +87,7 @@ func ActionUsers() Action {
// ActionGroups completes group names
func ActionGroups() Action {
return Action{
Bash: bash.ActionGroups(),
Fish: fish.ActionGroups(),
Zsh: zsh.ActionGroups(),
}
Expand All @@ -86,6 +96,7 @@ func ActionGroups() Action {
// ActionHosts completes host names
func ActionHosts() Action {
return Action{
Bash: bash.ActionHosts(),
Fish: fish.ActionHosts(),
Zsh: zsh.ActionHosts(),
}
Expand All @@ -94,6 +105,7 @@ func ActionHosts() Action {
// ActionOptions completes the names of shell options
func ActionOptions() Action {
return Action{
Bash: bash.ActionOptions(),
Fish: fish.ActionOptions(),
Zsh: zsh.ActionOptions(),
}
Expand All @@ -102,6 +114,7 @@ func ActionOptions() Action {
// ActionValues completes arbitrary keywords (values)
func ActionValues(values ...string) Action {
return Action{
Bash: bash.ActionValues(values...),
Fish: fish.ActionValues(values...),
Zsh: zsh.ActionValues(values...),
}
Expand All @@ -110,6 +123,7 @@ func ActionValues(values ...string) Action {
// ActionValuesDescribed completes arbitrary key (values) with an additional description (value, description pairs)
func ActionValuesDescribed(values ...string) Action {
return Action{
Bash: bash.ActionValuesDescribed(values...),
Fish: fish.ActionValuesDescribed(values...),
Zsh: zsh.ActionValuesDescribed(values...),
}
Expand All @@ -118,6 +132,7 @@ func ActionValuesDescribed(values ...string) Action {
// ActionMessage displays a help messages in places where no completions can be generated
func ActionMessage(msg string) Action {
return Action{
Bash: bash.ActionMessage(msg),
Fish: fish.ActionMessage(msg),
Zsh: zsh.ActionMessage(msg),
}
Expand All @@ -126,6 +141,7 @@ func ActionMessage(msg string) Action {
// ActionMultiParts completes multiple parts of words separately where each part is separated by some char
func ActionMultiParts(separator rune, values ...string) Action {
return Action{
Bash: bash.ActionMultiParts(separator, values...),
Fish: fish.ActionMultiParts(separator, values...),
Zsh: zsh.ActionMultiParts(separator, values...),
}
Expand Down
79 changes: 79 additions & 0 deletions bash/bash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package bash

import (
"fmt"
"strings"
)

func Callback(uid string) string {
return fmt.Sprintf(`eval $(_callback '%v')`, uid) // TODO update and use ActionExecute for eval?
}

func ActionExecute(command string) string {
return fmt.Sprintf(`$(%v)`, command)
}

func ActionBool() string {
return ActionValues("true", "false")
}

func ActionPathFiles(suffix string) string {
return ""
}

func ActionFiles(suffix string) string {
return fmt.Sprintf(`compgen -f -o plusdirs -X "!*%v" -- $last`, suffix)
}

func ActionNetInterfaces() string {
return ""
}

func ActionUsers() string {
return `compgen -u -- $last`
}

func ActionGroups() string {
return `compgen -g -- $last`
}

func ActionHosts() string {
return ""
}

func ActionOptions() string {
return ""
}

func ActionValues(values ...string) string {
if len(strings.TrimSpace(strings.Join(values, ""))) == 0 {
return ActionMessage("no values to complete")
}

vals := make([]string, len(values))
for index, val := range values {
// TODO escape special characters
//vals[index] = strings.Replace(val, " ", `\ `, -1)
vals[index] = val
}
return fmt.Sprintf(`compgen -W "%v" -- $last`, strings.Join(vals, ` `))
}

func ActionValuesDescribed(values ...string) string {
// TODO verify length (description always exists)
vals := make([]string, len(values))
for index, val := range values {
if index%2 == 0 {
vals[index/2] = val
}
}
return ActionValues(vals...)
}

func ActionMessage(msg string) string {
return ActionValues("ERR", msg) // TODO escape characters
}

func ActionMultiParts(separator rune, values ...string) string {
return ActionValues(values...)
}
39 changes: 39 additions & 0 deletions bash/snippet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package bash

import (
"fmt"

"github.com/spf13/pflag"
)

func SnippetFlagList(flags *pflag.FlagSet) string {
flagValues := make([]string, 0)

flags.VisitAll(func(flag *pflag.Flag) {
if !flag.Hidden {
flagValues = append(flagValues, "--"+flag.Name)
if flag.Shorthand != "" {
flagValues = append(flagValues, "-"+flag.Shorthand)
}
}
})
return ActionValues(flagValues...)
}

func SnippetFlagCompletion(flag *pflag.Flag, action string) (snippet string) {
if flag.NoOptDefVal != "" {
return ""
}

var names string
if flag.Shorthand != "" {
names = fmt.Sprintf("-%v | --%v", flag.Shorthand, flag.Name)
} else {
names = "--" + flag.Name
}

return fmt.Sprintf(` %v)
COMPREPLY=($(%v))
;;
`, names, action)
}
108 changes: 107 additions & 1 deletion carapace.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"strings"

"github.com/rsteube/carapace/bash"
"github.com/rsteube/carapace/fish"
"github.com/rsteube/carapace/uid"
"github.com/rsteube/carapace/zsh"
Expand Down Expand Up @@ -177,6 +178,75 @@ func (c Completions) GenerateFishFunctions(cmd *cobra.Command) string {

//fish

// bash
func (c Completions) GenerateBash(cmd *cobra.Command) string {
result := fmt.Sprintf(`#!/bin/bash
_callback() {
local compline="${COMP_LINE:0:${COMP_POINT}}"
echo "$compline" | sed "s/ \$/ _/" | xargs %v _carapace bash "$1"
}
_completions() {
local compline="${COMP_LINE:0:${COMP_POINT}}"
local state=$(echo "$compline" | sed "s/ \$/ _/" | xargs %v _carapace bash state)
local last="${COMP_WORDS[${COMP_CWORD}]}"
local previous="${COMP_WORDS[$((${COMP_CWORD}-1))]}"
case $state in
%v
esac
}
complete -F _completions %v
`, cmd.Name(), cmd.Name(), c.GenerateBashFunctions(cmd), cmd.Name())

return result
}

func (c Completions) GenerateBashFunctions(cmd *cobra.Command) string {
// TODO ensure state is only called oncy per LINE
function_pattern := `
'%v' )
if [[ $last == -* ]]; then
COMPREPLY=($(%v))
else
case $previous in
%v
*)
COMPREPLY=($(%v))
;;
esac
fi
;;
`

flags := make([]string, 0)
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
if !f.Hidden {
var s string
if action, ok := c.actions[uid.Flag(cmd, f)]; ok {
s = bash.SnippetFlagCompletion(f, action.Bash)
} else {
s = bash.SnippetFlagCompletion(f, "")
}
flags = append(flags, s)
}
})

result := make([]string, 0)
// uid.Command, flagList, genflagcompletions, commandargumentcompletion
result = append(result, fmt.Sprintf(function_pattern, uid.Command(cmd), bash.SnippetFlagList(cmd.LocalFlags()), strings.Join(flags, "\n"), bash.Callback("_")))
for _, subcmd := range cmd.Commands() {
if !subcmd.Hidden {
result = append(result, c.GenerateBashFunctions(subcmd))
}
}
return strings.Join(result, "\n")
}

// bash

func flagAlreadySet(cmd *cobra.Command, flag *pflag.Flag) bool {
if cmd.LocalFlags().Lookup(flag.Name) != nil {
return false
Expand Down Expand Up @@ -253,7 +323,43 @@ func addCompletionCommand(cmd *cobra.Command) {
_, targetArgs := traverse(cmd, origArg)
fmt.Println(completions.invokeCallback(callback, targetArgs).Zsh)
}

} else if args[0] == "bash" {
if len(args) <= 1 {
fmt.Println(completions.GenerateBash(cmd.Root()))
} else {
callback := args[1]
origArg := []string{}
if len(os.Args) > 5 {
origArg = os.Args[5:]
}
targetCmd, targetArgs := traverse(cmd, origArg)
if callback == "_" {
if len(targetArgs) == 0 {
callback = uid.Positional(targetCmd, 1)
} else {
lastArg := targetArgs[len(targetArgs)-1]
if strings.HasSuffix(lastArg, " ") {
callback = uid.Positional(targetCmd, len(targetArgs)+1)
} else {
callback = uid.Positional(targetCmd, len(targetArgs))
}
}
if _, ok := completions.actions[callback]; !ok {
if targetCmd.HasSubCommands() && len(targetArgs) <= 1 {
subcommands := make([]string, len(targetCmd.Commands()))
for i, c := range targetCmd.Commands() {
subcommands[i] = c.Name() // TODO alias
}
fmt.Println(bash.ActionValues(subcommands...))
}
os.Exit(0) // ensure no message for missing action on positional completion
}
} else if callback == "state" {
fmt.Println(uid.Command(targetCmd))
os.Exit(0) // TODO
}
fmt.Println(completions.invokeCallback(callback, targetArgs).Bash)
}
} else { // fish
// fish
if len(args) <= 1 {
Expand Down

0 comments on commit daba7c3

Please sign in to comment.