mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 03:03:32 +01:00
feat: migrate from docopt to cobra for CLI argument parsing
Replace docopt-go with spf13/cobra, giving cheat a built-in `--completion` flag that dynamically generates shell completions for bash, zsh, fish, and powershell. This replaces the manually-maintained static completion scripts and resolves the root cause of multiple completion issues (#768, #705, #633, #632, #476). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
43
internal/completions/cheatsheets.go
Normal file
43
internal/completions/cheatsheets.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Package completions provides dynamic shell completion functions and
|
||||
// completion script generation for the cheat CLI.
|
||||
package completions
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
// Cheatsheets provides completion for cheatsheet names.
|
||||
func Cheatsheets(
|
||||
_ *cobra.Command,
|
||||
args []string,
|
||||
_ string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
conf, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
consolidated := sheets.Consolidate(cheatsheets)
|
||||
|
||||
names := make([]string, 0, len(consolidated))
|
||||
for name := range consolidated {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
38
internal/completions/config.go
Normal file
38
internal/completions/config.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/cheat/cheat/internal/config"
|
||||
)
|
||||
|
||||
// loadConfig loads the cheat configuration for use in completion functions.
|
||||
// It returns an error rather than exiting, since completions should degrade
|
||||
// gracefully.
|
||||
func loadConfig() (config.Config, error) {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
envvars := config.EnvVars()
|
||||
|
||||
confpaths, err := config.Paths(runtime.GOOS, home, envvars)
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
confpath, err := config.Path(confpaths)
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
conf, err := config.New(confpath, true)
|
||||
if err != nil {
|
||||
return config.Config{}, err
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
24
internal/completions/generate.go
Normal file
24
internal/completions/generate.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Generate writes a shell completion script to the given writer.
|
||||
func Generate(cmd *cobra.Command, shell string, w io.Writer) error {
|
||||
switch shell {
|
||||
case "bash":
|
||||
return cmd.Root().GenBashCompletionV2(w, true)
|
||||
case "zsh":
|
||||
return cmd.Root().GenZshCompletion(w)
|
||||
case "fish":
|
||||
return cmd.Root().GenFishCompletion(w, true)
|
||||
case "powershell":
|
||||
return cmd.Root().GenPowerShellCompletionWithDesc(w)
|
||||
default:
|
||||
return fmt.Errorf("unsupported shell: %s (valid: bash, zsh, fish, powershell)", shell)
|
||||
}
|
||||
}
|
||||
25
internal/completions/paths.go
Normal file
25
internal/completions/paths.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Paths provides completion for the --path flag.
|
||||
func Paths(
|
||||
_ *cobra.Command,
|
||||
_ []string,
|
||||
_ string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
conf, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(conf.Cheatpaths))
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
names = append(names, cp.Name)
|
||||
}
|
||||
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
27
internal/completions/tags.go
Normal file
27
internal/completions/tags.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package completions
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cheat/cheat/internal/sheets"
|
||||
)
|
||||
|
||||
// Tags provides completion for the --tag flag.
|
||||
func Tags(
|
||||
_ *cobra.Command,
|
||||
_ []string,
|
||||
_ string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
conf, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
cheatsheets, err := sheets.Load(conf.Cheatpaths)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
return sheets.Tags(cheatsheets), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// Color indicates whether colorization should be applied to the output
|
||||
func (c *Config) Color(opts map[string]interface{}) bool {
|
||||
func (c *Config) Color(forceColorize bool) bool {
|
||||
|
||||
// default to the colorization specified in the configs...
|
||||
colorize := c.Colorize
|
||||
@@ -18,7 +18,7 @@ func (c *Config) Color(opts map[string]interface{}) bool {
|
||||
}
|
||||
|
||||
// ... *unless* the --colorize flag was passed
|
||||
if opts["--colorize"] == true {
|
||||
if forceColorize {
|
||||
colorize = true
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ func TestColor(t *testing.T) {
|
||||
// mock a config
|
||||
conf := Config{}
|
||||
|
||||
opts := map[string]interface{}{"--colorize": false}
|
||||
if conf.Color(opts) {
|
||||
t.Errorf("failed to respect --colorize (false)")
|
||||
if conf.Color(false) {
|
||||
t.Errorf("failed to respect forceColorize (false)")
|
||||
}
|
||||
|
||||
opts = map[string]interface{}{"--colorize": true}
|
||||
if !conf.Color(opts) {
|
||||
t.Errorf("failed to respect --colorize (true)")
|
||||
if !conf.Color(true) {
|
||||
t.Errorf("failed to respect forceColorize (true)")
|
||||
}
|
||||
}
|
||||
|
||||
20
internal/config/env.go
Normal file
20
internal/config/env.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnvVars reads environment variables into a map of strings.
|
||||
func EnvVars() map[string]string {
|
||||
envvars := map[string]string{}
|
||||
for _, e := range os.Environ() {
|
||||
pair := strings.SplitN(e, "=", 2)
|
||||
if runtime.GOOS == "windows" {
|
||||
pair[0] = strings.ToUpper(pair[0])
|
||||
}
|
||||
envvars[pair[0]] = pair[1]
|
||||
}
|
||||
return envvars
|
||||
}
|
||||
Reference in New Issue
Block a user