Skip to content
This repository was archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Add multi-shell command completion because at this point why not
Browse files Browse the repository at this point in the history
  • Loading branch information
blakewatters committed Apr 19, 2020
1 parent 51abd7b commit 636079b
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ clean:
install: build
$(info ******************** installing ********************)
cp $(BIN)/opsani /usr/local/bin/opsani

completion:
$(info ******************** completion ********************)
go run . completion --shell zsh > /usr/local/share/zsh-completions/_opsani
4 changes: 4 additions & 0 deletions cmd/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,15 @@ func init() {

// app config flags
appConfigCmd.Flags().StringVarP(&appConfig.OutputFile, "output", "o", "", "Write output to file instead of stdout")
appConfigCmd.MarkFlagFilename("output")

// app config set & patch flags
updateGlobs := []string{"*.json", "*.yaml", "*.yml"}
appConfigPatchCmd.Flags().StringVarP(&appConfig.InputFile, "file", "f", "", "File containing config to apply")
appConfigPatchCmd.MarkFlagFilename("file", updateGlobs...)
appConfigPatchCmd.Flags().BoolVarP(&appConfig.ApplyNow, "apply", "a", true, "Apply the config changes immediately")
appConfigSetCmd.Flags().StringVarP(&appConfig.InputFile, "file", "f", "", "File containing config to apply")
appConfigSetCmd.MarkFlagFilename("file", updateGlobs...)
appConfigSetCmd.Flags().BoolVarP(&appConfig.ApplyNow, "apply", "a", true, "Apply the config changes immediately")

// app edit flags
Expand Down
71 changes: 71 additions & 0 deletions cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cmd

import (
"errors"
"fmt"
"os"

"github.com/mattn/go-isatty"
"github.com/opsani/cli/internal/cobrafish"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(completionCmd)
completionCmd.Flags().StringP("shell", "s", "", "Shell type: {bash|zsh|fish|powershell}")
}

var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generate shell completion scripts",
Long: `Generate shell completion scripts for Opsani CLI commands.
The output of this command will be computer code and is meant to be saved to a
file or immediately evaluated by an interactive shell.
For example, for bash you could add this to your '~/.bash_profile':
eval "$(gh completion -s bash)"
When installing Opsani CLI through a package manager, however, it's possible that
no additional shell configuration is necessary to gain completion support. For
Homebrew, see <https://docs.brew.sh/Shell-Completion>
`,
RunE: func(cmd *cobra.Command, args []string) error {
shellType, err := cmd.Flags().GetString("shell")
if err != nil {
return err
}

if shellType == "" {
out := cmd.OutOrStdout()
isTTY := false
if outFile, isFile := out.(*os.File); isFile {
isTTY = IsTerminal(outFile)
}

if isTTY {
return errors.New("error: the value for `--shell` is required\nsee `opsani help completion` for more information")
}
shellType = "bash"
}

switch shellType {
case "bash":
return rootCmd.GenBashCompletion(cmd.OutOrStdout())
case "zsh":
return rootCmd.GenZshCompletion(cmd.OutOrStdout())
case "powershell":
return rootCmd.GenPowerShellCompletion(cmd.OutOrStdout())
case "fish":
return cobrafish.GenCompletion(rootCmd, cmd.OutOrStdout())
default:
return fmt.Errorf("unsupported shell type %q", shellType)
}
},
}

// IsTerminal reports whether the file descriptor is connected to a terminal
func IsTerminal(f *os.File) bool {
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
}
1 change: 1 addition & 0 deletions cmd/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,5 @@ func init() {
imbCmd.Flags().StringVarP(&discoverConfig.DockerImageRef, "image", "i", defaultImageRef, "Docker image ref to run")
imbCmd.Flags().StringVarP(&discoverConfig.DockerHost, "host", "H", "", "Docket host to connect to (overriding DOCKER_HOST)")
discoverCmd.Flags().StringVar(&discoverConfig.Kubeconfig, "kubeconfig", "", fmt.Sprintf("Location of the kubeconfig file (default is \"%s\")", pathToDefaultKubeconfig()))
discoverCmd.MarkFlagFilename("kubeconfig")
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func init() {
viper.BindPFlag(opsani.KeyRequestTracing, rootCmd.PersistentFlags().Lookup(opsani.KeyRequestTracing))

rootCmd.PersistentFlags().StringVar(&opsani.ConfigFile, "config", "", fmt.Sprintf("Location of config file (default \"%s\")", opsani.DefaultConfigFile()))
rootCmd.MarkPersistentFlagFilename("config", "*.yaml", "*.yml")
rootCmd.SetVersionTemplate("Opsani CLI version {{.Version}}\n")

// See Execute()
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e
github.com/imdario/mergo v0.3.9 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.2.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
Expand Down
171 changes: 171 additions & 0 deletions internal/cobrafish/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// imported from https://github.com/spf13/cobra/pull/754
// author: Tim Reddehase

package cobrafish

import (
"bytes"
"fmt"
"io"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

func GenCompletion(c *cobra.Command, w io.Writer) error {
buf := new(bytes.Buffer)

writeFishPreamble(c, buf)
writeFishCommandCompletion(c, c, buf)

_, err := buf.WriteTo(w)
return err
}

func writeFishPreamble(cmd *cobra.Command, buf *bytes.Buffer) {
subCommandNames := []string{}
rangeCommands(cmd, func(subCmd *cobra.Command) {
subCommandNames = append(subCommandNames, subCmd.Name())
})
buf.WriteString(fmt.Sprintf(`
function __fish_%s_no_subcommand --description 'Test if %s has yet to be given the subcommand'
for i in (commandline -opc)
if contains -- $i %s
return 1
end
end
return 0
end
function __fish_%s_seen_subcommand_path --description 'Test whether the full path of subcommands is the current path'
set -l cmd (commandline -opc)
set -e cmd[1]
set -l pattern (string replace -a " " ".+" "$argv")
string match -r "$pattern" (string trim -- "$cmd")
end
# borrowed from current fish-shell master, since it is not in current 2.7.1 release
function __fish_seen_argument
argparse 's/short=+' 'l/long=+' -- $argv
set cmd (commandline -co)
set -e cmd[1]
for t in $cmd
for s in $_flag_s
if string match -qr "^-[A-z0-9]*"$s"[A-z0-9]*\$" -- $t
return 0
end
end
for l in $_flag_l
if string match -q -- "--$l" $t
return 0
end
end
end
return 1
end
`, cmd.Name(), cmd.Name(), strings.Join(subCommandNames, " "), cmd.Name()))
}

func writeFishCommandCompletion(rootCmd, cmd *cobra.Command, buf *bytes.Buffer) {
rangeCommands(cmd, func(subCmd *cobra.Command) {
condition := commandCompletionCondition(rootCmd, cmd)
escapedDescription := strings.Replace(subCmd.Short, "'", "\\'", -1)
buf.WriteString(fmt.Sprintf("complete -c %s -f %s -a %s -d '%s'\n", rootCmd.Name(), condition, subCmd.Name(), escapedDescription))
})
for _, validArg := range append(cmd.ValidArgs, cmd.ArgAliases...) {
condition := commandCompletionCondition(rootCmd, cmd)
buf.WriteString(
fmt.Sprintf("complete -c %s -f %s -a %s -d '%s'\n",
rootCmd.Name(), condition, validArg, fmt.Sprintf("Positional Argument to %s", cmd.Name())))
}
writeCommandFlagsCompletion(rootCmd, cmd, buf)
rangeCommands(cmd, func(subCmd *cobra.Command) {
writeFishCommandCompletion(rootCmd, subCmd, buf)
})
}

func writeCommandFlagsCompletion(rootCmd, cmd *cobra.Command, buf *bytes.Buffer) {
cmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
if nonCompletableFlag(flag) {
return
}
writeCommandFlagCompletion(rootCmd, cmd, buf, flag)
})
cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
if nonCompletableFlag(flag) {
return
}
writeCommandFlagCompletion(rootCmd, cmd, buf, flag)
})
}

func writeCommandFlagCompletion(rootCmd, cmd *cobra.Command, buf *bytes.Buffer, flag *pflag.Flag) {
shortHandPortion := ""
if len(flag.Shorthand) > 0 {
shortHandPortion = fmt.Sprintf("-s %s", flag.Shorthand)
}
condition := completionCondition(rootCmd, cmd)
escapedUsage := strings.Replace(flag.Usage, "'", "\\'", -1)
buf.WriteString(fmt.Sprintf("complete -c %s -f %s %s %s -l %s -d '%s'\n",
rootCmd.Name(), condition, flagRequiresArgumentCompletion(flag), shortHandPortion, flag.Name, escapedUsage))
}

func flagRequiresArgumentCompletion(flag *pflag.Flag) string {
if flag.Value.Type() != "bool" {
return "-r"
}
return ""
}

func subCommandPath(rootCmd *cobra.Command, cmd *cobra.Command) string {
path := make([]string, 0, 1)
currentCmd := cmd
if rootCmd == cmd {
return ""
}
for {
path = append([]string{currentCmd.Name()}, path...)
if currentCmd.Parent() == rootCmd {
return strings.Join(path, " ")
}
currentCmd = currentCmd.Parent()
}
}

func rangeCommands(cmd *cobra.Command, callback func(subCmd *cobra.Command)) {
for _, subCmd := range cmd.Commands() {
if !subCmd.IsAvailableCommand() || strings.HasPrefix(subCmd.Use, "help") {
continue
}
callback(subCmd)
}
}

func commandCompletionCondition(rootCmd, cmd *cobra.Command) string {
localNonPersistentFlags := cmd.LocalNonPersistentFlags()
bareConditions := make([]string, 0, 1)
if rootCmd != cmd {
bareConditions = append(bareConditions, fmt.Sprintf("__fish_%s_seen_subcommand_path %s", rootCmd.Name(), subCommandPath(rootCmd, cmd)))
} else {
bareConditions = append(bareConditions, fmt.Sprintf("__fish_%s_no_subcommand", rootCmd.Name()))
}
localNonPersistentFlags.VisitAll(func(flag *pflag.Flag) {
flagSelector := fmt.Sprintf("-l %s", flag.Name)
if len(flag.Shorthand) > 0 {
flagSelector = fmt.Sprintf("-s %s %s", flag.Shorthand, flagSelector)
}
bareConditions = append(bareConditions, fmt.Sprintf("not __fish_seen_argument %s", flagSelector))
})
return fmt.Sprintf("-n '%s'", strings.Join(bareConditions, "; and "))
}

func completionCondition(rootCmd, cmd *cobra.Command) string {
condition := fmt.Sprintf("-n '__fish_%s_no_subcommand'", rootCmd.Name())
if rootCmd != cmd {
condition = fmt.Sprintf("-n '__fish_%s_seen_subcommand_path %s'", rootCmd.Name(), subCommandPath(rootCmd, cmd))
}
return condition
}

func nonCompletableFlag(flag *pflag.Flag) bool {
return flag.Hidden || len(flag.Deprecated) > 0
}

0 comments on commit 636079b

Please sign in to comment.